django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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.
Files changed (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,294 @@
1
+ """
2
+ API Access Control Middleware.
3
+ Handles API key authentication and subscription validation.
4
+ """
5
+
6
+ import logging
7
+ from typing import Optional, Tuple
8
+ from django.http import JsonResponse, HttpRequest, HttpResponse
9
+ from django.utils.deprecation import MiddlewareMixin
10
+ from django.conf import settings
11
+ from django.utils import timezone
12
+ from ..models import APIKey, Subscription, EndpointGroup
13
+ from ..services import ApiKeyCache, RateLimitCache
14
+ from ..services.security import error_handler, SecurityError
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class APIAccessMiddleware(MiddlewareMixin):
20
+ """
21
+ Middleware for API access control using API keys and subscriptions.
22
+
23
+ Features:
24
+ - API key validation
25
+ - Subscription status checking
26
+ - Endpoint access control
27
+ - Usage tracking
28
+ """
29
+
30
+ def __init__(self, get_response=None):
31
+ super().__init__(get_response)
32
+ self.api_key_cache = ApiKeyCache()
33
+ self.rate_limit_cache = RateLimitCache()
34
+
35
+ # Paths that don't require API key authentication
36
+ self.exempt_paths = getattr(settings, 'PAYMENTS_EXEMPT_PATHS', [
37
+ '/api/v1/api-key/validate/',
38
+ '/api/v1/api-key/create/',
39
+ '/admin/',
40
+ '/cfg/',
41
+ ])
42
+
43
+ # API prefixes that require authentication
44
+ self.api_prefixes = getattr(settings, 'PAYMENTS_API_PREFIXES', [
45
+ '/api/v1/',
46
+ ])
47
+
48
+ def process_request(self, request: HttpRequest) -> Optional[JsonResponse]:
49
+ """Process incoming request for API access control."""
50
+
51
+ # Skip non-API requests
52
+ if not self._is_api_request(request):
53
+ return None
54
+
55
+ # Skip exempt paths
56
+ if self._is_exempt_path(request):
57
+ return None
58
+
59
+ # Extract API key
60
+ api_key = self._extract_api_key(request)
61
+ if not api_key:
62
+ security_error = SecurityError(
63
+ "API key required for protected endpoint",
64
+ details={'path': request.path, 'method': request.method}
65
+ )
66
+ error_handler.handle_error(security_error, {
67
+ 'middleware': 'api_access',
68
+ 'operation': 'api_key_extraction'
69
+ }, request)
70
+
71
+ return self._error_response(
72
+ 'API key required',
73
+ status=401,
74
+ error_code='MISSING_API_KEY'
75
+ )
76
+
77
+ # Validate API key
78
+ api_key_obj = self._validate_api_key(api_key)
79
+ if not api_key_obj:
80
+ security_error = SecurityError(
81
+ f"Invalid or expired API key attempted",
82
+ details={
83
+ 'api_key_prefix': api_key[:8] + '...' if len(api_key) > 8 else api_key,
84
+ 'path': request.path,
85
+ 'method': request.method,
86
+ 'ip_address': self._get_client_ip(request)
87
+ }
88
+ )
89
+ error_handler.handle_error(security_error, {
90
+ 'middleware': 'api_access',
91
+ 'operation': 'api_key_validation'
92
+ }, request)
93
+
94
+ return self._error_response(
95
+ 'Invalid or expired API key',
96
+ status=401,
97
+ error_code='INVALID_API_KEY'
98
+ )
99
+
100
+ # Check subscription access
101
+ endpoint_group = self._get_endpoint_group(request)
102
+ if endpoint_group:
103
+ subscription = self._check_subscription_access(api_key_obj.user, endpoint_group)
104
+ if not subscription:
105
+ return self._error_response(
106
+ f'No active subscription for {endpoint_group.display_name}',
107
+ status=403,
108
+ error_code='NO_SUBSCRIPTION'
109
+ )
110
+
111
+ # Check usage limits
112
+ if self._is_usage_exceeded(subscription):
113
+ return self._error_response(
114
+ 'Usage limit exceeded for this subscription',
115
+ status=429,
116
+ error_code='USAGE_EXCEEDED'
117
+ )
118
+
119
+ # Store subscription in request for usage tracking
120
+ request.payment_subscription = subscription
121
+
122
+ # Store API key in request
123
+ request.payment_api_key = api_key_obj
124
+ request.payment_user = api_key_obj.user
125
+
126
+ return None
127
+
128
+ def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
129
+ """Process response to track API usage."""
130
+
131
+ # Track usage if API key was used
132
+ if hasattr(request, 'payment_api_key') and hasattr(request, 'payment_subscription'):
133
+ self._track_usage(request.payment_api_key, request.payment_subscription, request)
134
+
135
+ return response
136
+
137
+ def _is_api_request(self, request: HttpRequest) -> bool:
138
+ """Check if request is an API request."""
139
+ path = request.path
140
+ return any(path.startswith(prefix) for prefix in self.api_prefixes)
141
+
142
+ def _is_exempt_path(self, request: HttpRequest) -> bool:
143
+ """Check if path is exempt from API key requirement."""
144
+ path = request.path
145
+ return any(path.startswith(exempt) for exempt in self.exempt_paths)
146
+
147
+ def _extract_api_key(self, request: HttpRequest) -> Optional[str]:
148
+ """Extract API key from request headers or query params."""
149
+
150
+ # Try Authorization header first (Bearer token)
151
+ auth_header = request.META.get('HTTP_AUTHORIZATION', '')
152
+ if auth_header.startswith('Bearer '):
153
+ return auth_header[7:] # Remove 'Bearer ' prefix
154
+
155
+ # Try X-API-Key header
156
+ api_key = request.META.get('HTTP_X_API_KEY')
157
+ if api_key:
158
+ return api_key
159
+
160
+ # Try query parameter (less secure, for testing)
161
+ api_key = request.GET.get('api_key')
162
+ if api_key:
163
+ return api_key
164
+
165
+ return None
166
+
167
+ def _validate_api_key(self, api_key: str) -> Optional[APIKey]:
168
+ """Validate API key using Redis cache with DB fallback."""
169
+
170
+ try:
171
+ # Try Redis first
172
+ cached_key = self.redis_service.get_api_key(api_key)
173
+ if cached_key:
174
+ return cached_key
175
+
176
+ # Fallback to database
177
+ api_key_obj = APIKey.objects.select_related('user').filter(
178
+ key_value=api_key,
179
+ is_active=True
180
+ ).first()
181
+
182
+ if api_key_obj and api_key_obj.is_valid():
183
+ # Cache valid key
184
+ self.redis_service.cache_api_key(api_key_obj)
185
+
186
+ # Update last used
187
+ api_key_obj.record_usage()
188
+
189
+ return api_key_obj
190
+
191
+ return None
192
+
193
+ except Exception as e:
194
+ logger.error(f"Error validating API key: {e}")
195
+ return None
196
+
197
+ def _get_endpoint_group(self, request: HttpRequest) -> Optional[EndpointGroup]:
198
+ """Determine endpoint group based on request path."""
199
+
200
+ # This would be customized per project
201
+ # For now, return None (no endpoint group restrictions)
202
+ # In real implementation, this would map URL patterns to endpoint groups
203
+
204
+ path = request.path
205
+
206
+ # Example mapping (would be configurable)
207
+ endpoint_mappings = {
208
+ '/api/v1/payments/': 'payments',
209
+ '/api/v1/subscriptions/': 'billing',
210
+ '/api/v1/users/': 'user_management',
211
+ }
212
+
213
+ for path_prefix, group_name in endpoint_mappings.items():
214
+ if path.startswith(path_prefix):
215
+ try:
216
+ return EndpointGroup.objects.get(name=group_name, is_active=True)
217
+ except EndpointGroup.DoesNotExist:
218
+ continue
219
+
220
+ return None
221
+
222
+ def _check_subscription_access(self, user, endpoint_group: EndpointGroup) -> Optional[Subscription]:
223
+ """Check if user has active subscription for endpoint group."""
224
+
225
+ try:
226
+ # Get active subscription for this endpoint group
227
+ subscription = Subscription.objects.filter(
228
+ user=user,
229
+ endpoint_group=endpoint_group,
230
+ status='active',
231
+ expires_at__gt=timezone.now()
232
+ ).first()
233
+
234
+ return subscription
235
+
236
+ except Exception as e:
237
+ logger.error(f"Error checking subscription access: {e}")
238
+ return None
239
+
240
+ def _is_usage_exceeded(self, subscription: Subscription) -> bool:
241
+ """Check if subscription usage limit is exceeded."""
242
+
243
+ try:
244
+ # Check current usage against limit
245
+ if subscription.usage_limit == 0: # Unlimited
246
+ return False
247
+
248
+ return subscription.usage_current >= subscription.usage_limit
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error checking usage limits: {e}")
252
+ return False
253
+
254
+ def _track_usage(self, api_key: APIKey, subscription: Subscription, request: HttpRequest):
255
+ """Track API usage for billing and analytics."""
256
+
257
+ try:
258
+ # Increment subscription usage
259
+ subscription.usage_current += 1
260
+ subscription.save(update_fields=['usage_current'])
261
+
262
+ # Update Redis cache
263
+ self.redis_service.increment_usage(api_key.key_value, subscription.id)
264
+
265
+ # Log usage for analytics
266
+ logger.info(
267
+ f"API usage tracked - User: {api_key.user.id}, "
268
+ f"Subscription: {subscription.id}, "
269
+ f"Path: {request.path}, "
270
+ f"Usage: {subscription.usage_current}/{subscription.usage_limit}"
271
+ )
272
+
273
+ except Exception as e:
274
+ logger.error(f"Error tracking usage: {e}")
275
+
276
+ def _get_client_ip(self, request: HttpRequest) -> str:
277
+ """Extract client IP address from request."""
278
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
279
+ if x_forwarded_for:
280
+ ip = x_forwarded_for.split(',')[0].strip()
281
+ else:
282
+ ip = request.META.get('REMOTE_ADDR', '')
283
+ return ip
284
+
285
+ def _error_response(self, message: str, status: int = 400, error_code: str = 'ERROR') -> JsonResponse:
286
+ """Return standardized error response."""
287
+
288
+ return JsonResponse({
289
+ 'error': {
290
+ 'code': error_code,
291
+ 'message': message,
292
+ 'timestamp': timezone.now().isoformat(),
293
+ }
294
+ }, status=status)
@@ -0,0 +1,216 @@
1
+ """
2
+ Rate Limiting Middleware.
3
+ Implements sliding window rate limiting using Redis.
4
+ """
5
+
6
+ import logging
7
+ import time
8
+ from typing import Optional
9
+ from django.http import JsonResponse, HttpRequest
10
+ from django.utils.deprecation import MiddlewareMixin
11
+ from django.conf import settings
12
+ from django.utils import timezone
13
+ from ..services import RateLimitCache
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class RateLimitingMiddleware(MiddlewareMixin):
19
+ """
20
+ Rate limiting middleware using sliding window algorithm.
21
+
22
+ Features:
23
+ - Per-API-key rate limiting
24
+ - Per-IP rate limiting (fallback)
25
+ - Sliding window algorithm
26
+ - Redis-based with circuit breaker
27
+ - Configurable limits
28
+ """
29
+
30
+ def __init__(self, get_response=None):
31
+ super().__init__(get_response)
32
+ self.rate_limit_cache = RateLimitCache()
33
+
34
+ # Default rate limits (can be overridden in settings)
35
+ self.default_limits = getattr(settings, 'PAYMENTS_RATE_LIMITS', {
36
+ 'per_minute': 60, # 60 requests per minute
37
+ 'per_hour': 1000, # 1000 requests per hour
38
+ 'per_day': 10000, # 10000 requests per day
39
+ })
40
+
41
+ # Paths exempt from rate limiting
42
+ self.exempt_paths = getattr(settings, 'PAYMENTS_RATE_LIMIT_EXEMPT_PATHS', [
43
+ '/admin/',
44
+ '/cfg/',
45
+ ])
46
+
47
+ # Enable/disable rate limiting
48
+ self.enabled = getattr(settings, 'PAYMENTS_RATE_LIMITING_ENABLED', True)
49
+
50
+ def process_request(self, request: HttpRequest) -> Optional[JsonResponse]:
51
+ """Process request for rate limiting."""
52
+
53
+ if not self.enabled:
54
+ return None
55
+
56
+ # Skip exempt paths
57
+ if self._is_exempt_path(request):
58
+ return None
59
+
60
+ # Get rate limiting key (API key or IP)
61
+ rate_key = self._get_rate_key(request)
62
+ if not rate_key:
63
+ return None
64
+
65
+ # Get rate limits for this key
66
+ limits = self._get_rate_limits(request)
67
+
68
+ # Check each time window
69
+ for window, limit in limits.items():
70
+ if self._is_rate_limited(rate_key, window, limit):
71
+ return self._rate_limit_response(window, limit)
72
+
73
+ # Record this request
74
+ self._record_request(rate_key)
75
+
76
+ return None
77
+
78
+ def _is_exempt_path(self, request: HttpRequest) -> bool:
79
+ """Check if path is exempt from rate limiting."""
80
+ path = request.path
81
+ return any(path.startswith(exempt) for exempt in self.exempt_paths)
82
+
83
+ def _get_rate_key(self, request: HttpRequest) -> Optional[str]:
84
+ """Get rate limiting key (API key preferred, IP as fallback)."""
85
+
86
+ # Use API key if available (from previous middleware)
87
+ if hasattr(request, 'payment_api_key'):
88
+ return f"api_key:{request.payment_api_key.key_value}"
89
+
90
+ # Fallback to IP address
91
+ ip = self._get_client_ip(request)
92
+ if ip:
93
+ return f"ip:{ip}"
94
+
95
+ return None
96
+
97
+ def _get_client_ip(self, request: HttpRequest) -> Optional[str]:
98
+ """Get client IP address."""
99
+
100
+ # Check for forwarded headers first
101
+ forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
102
+ if forwarded_for:
103
+ return forwarded_for.split(',')[0].strip()
104
+
105
+ # Check for real IP header
106
+ real_ip = request.META.get('HTTP_X_REAL_IP')
107
+ if real_ip:
108
+ return real_ip
109
+
110
+ # Fallback to remote address
111
+ return request.META.get('REMOTE_ADDR')
112
+
113
+ def _get_rate_limits(self, request: HttpRequest) -> dict:
114
+ """Get rate limits for this request."""
115
+
116
+ # Check if API key has custom limits
117
+ if hasattr(request, 'payment_api_key'):
118
+ api_key = request.payment_api_key
119
+
120
+ # Check if user has subscription with custom limits
121
+ if hasattr(request, 'payment_subscription'):
122
+ subscription = request.payment_subscription
123
+ # Custom limits based on subscription tier could be implemented here
124
+ # For now, use default limits
125
+ pass
126
+
127
+ return self.default_limits
128
+
129
+ def _is_rate_limited(self, rate_key: str, window: str, limit: int) -> bool:
130
+ """Check if rate limit is exceeded for given window."""
131
+
132
+ try:
133
+ # Get window duration in seconds
134
+ window_seconds = self._get_window_seconds(window)
135
+ if not window_seconds:
136
+ return False
137
+
138
+ # Use Redis sliding window
139
+ current_time = int(time.time())
140
+ window_start = current_time - window_seconds
141
+
142
+ # Get request count in window
143
+ redis_key = f"rate_limit:{rate_key}:{window}"
144
+
145
+ # Use Redis sorted set for sliding window
146
+ # Remove old entries and count current entries
147
+ count = self.redis_service.sliding_window_count(
148
+ redis_key,
149
+ window_start,
150
+ current_time,
151
+ window_seconds
152
+ )
153
+
154
+ return count >= limit
155
+
156
+ except Exception as e:
157
+ logger.error(f"Error checking rate limit: {e}")
158
+ # On error, allow request (fail open)
159
+ return False
160
+
161
+ def _get_window_seconds(self, window: str) -> Optional[int]:
162
+ """Convert window name to seconds."""
163
+
164
+ window_map = {
165
+ 'per_minute': 60,
166
+ 'per_hour': 3600,
167
+ 'per_day': 86400,
168
+ }
169
+
170
+ return window_map.get(window)
171
+
172
+ def _record_request(self, rate_key: str):
173
+ """Record request for rate limiting."""
174
+
175
+ try:
176
+ current_time = int(time.time())
177
+
178
+ # Record for each window
179
+ for window in self.default_limits.keys():
180
+ window_seconds = self._get_window_seconds(window)
181
+ if window_seconds:
182
+ redis_key = f"rate_limit:{rate_key}:{window}"
183
+
184
+ # Add current timestamp to sorted set
185
+ self.redis_service.record_request(
186
+ redis_key,
187
+ current_time,
188
+ window_seconds
189
+ )
190
+
191
+ except Exception as e:
192
+ logger.error(f"Error recording request: {e}")
193
+
194
+ def _rate_limit_response(self, window: str, limit: int) -> JsonResponse:
195
+ """Return rate limit exceeded response."""
196
+
197
+ window_seconds = self._get_window_seconds(window)
198
+ retry_after = window_seconds if window_seconds else 60
199
+
200
+ response = JsonResponse({
201
+ 'error': {
202
+ 'code': 'RATE_LIMIT_EXCEEDED',
203
+ 'message': f'Rate limit exceeded: {limit} requests {window}',
204
+ 'limit': limit,
205
+ 'window': window,
206
+ 'retry_after': retry_after,
207
+ 'timestamp': timezone.now().isoformat(),
208
+ }
209
+ }, status=429)
210
+
211
+ # Add rate limit headers
212
+ response['X-RateLimit-Limit'] = str(limit)
213
+ response['X-RateLimit-Window'] = window
214
+ response['Retry-After'] = str(retry_after)
215
+
216
+ return response