django-cfg 1.2.21__py3-none-any.whl → 1.2.23__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 (92) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/newsletter/signals.py +9 -8
  3. django_cfg/apps/payments/__init__.py +8 -0
  4. django_cfg/apps/payments/admin/__init__.py +23 -0
  5. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  6. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  7. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  8. django_cfg/apps/payments/admin/filters.py +259 -0
  9. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  10. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  11. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  12. django_cfg/apps/payments/apps.py +22 -0
  13. django_cfg/apps/payments/config/__init__.py +87 -0
  14. django_cfg/apps/payments/config/module.py +162 -0
  15. django_cfg/apps/payments/config/providers.py +93 -0
  16. django_cfg/apps/payments/config/settings.py +136 -0
  17. django_cfg/apps/payments/config/utils.py +198 -0
  18. django_cfg/apps/payments/decorators.py +291 -0
  19. django_cfg/apps/payments/managers/__init__.py +22 -0
  20. django_cfg/apps/payments/managers/api_key_manager.py +35 -0
  21. django_cfg/apps/payments/managers/balance_manager.py +361 -0
  22. django_cfg/apps/payments/managers/currency_manager.py +32 -0
  23. django_cfg/apps/payments/managers/payment_manager.py +44 -0
  24. django_cfg/apps/payments/managers/subscription_manager.py +37 -0
  25. django_cfg/apps/payments/managers/tariff_manager.py +29 -0
  26. django_cfg/apps/payments/middleware/__init__.py +13 -0
  27. django_cfg/apps/payments/middleware/api_access.py +261 -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 +1003 -0
  31. django_cfg/apps/payments/migrations/__init__.py +1 -0
  32. django_cfg/apps/payments/models/__init__.py +67 -0
  33. django_cfg/apps/payments/models/api_keys.py +96 -0
  34. django_cfg/apps/payments/models/balance.py +209 -0
  35. django_cfg/apps/payments/models/base.py +30 -0
  36. django_cfg/apps/payments/models/currencies.py +138 -0
  37. django_cfg/apps/payments/models/events.py +73 -0
  38. django_cfg/apps/payments/models/payments.py +301 -0
  39. django_cfg/apps/payments/models/subscriptions.py +270 -0
  40. django_cfg/apps/payments/models/tariffs.py +102 -0
  41. django_cfg/apps/payments/serializers/__init__.py +56 -0
  42. django_cfg/apps/payments/serializers/api_keys.py +51 -0
  43. django_cfg/apps/payments/serializers/balance.py +59 -0
  44. django_cfg/apps/payments/serializers/currencies.py +55 -0
  45. django_cfg/apps/payments/serializers/payments.py +62 -0
  46. django_cfg/apps/payments/serializers/subscriptions.py +71 -0
  47. django_cfg/apps/payments/serializers/tariffs.py +56 -0
  48. django_cfg/apps/payments/services/__init__.py +65 -0
  49. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  50. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  51. django_cfg/apps/payments/services/cache/base.py +30 -0
  52. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  53. django_cfg/apps/payments/services/core/__init__.py +17 -0
  54. django_cfg/apps/payments/services/core/balance_service.py +449 -0
  55. django_cfg/apps/payments/services/core/payment_service.py +393 -0
  56. django_cfg/apps/payments/services/core/subscription_service.py +616 -0
  57. django_cfg/apps/payments/services/internal_types.py +266 -0
  58. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  59. django_cfg/apps/payments/services/providers/__init__.py +19 -0
  60. django_cfg/apps/payments/services/providers/base.py +137 -0
  61. django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
  62. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  63. django_cfg/apps/payments/services/providers/registry.py +99 -0
  64. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  65. django_cfg/apps/payments/signals/__init__.py +13 -0
  66. django_cfg/apps/payments/signals/api_key_signals.py +150 -0
  67. django_cfg/apps/payments/signals/payment_signals.py +127 -0
  68. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  69. django_cfg/apps/payments/urls.py +78 -0
  70. django_cfg/apps/payments/utils/__init__.py +42 -0
  71. django_cfg/apps/payments/utils/config_utils.py +243 -0
  72. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  73. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  74. django_cfg/apps/payments/views/__init__.py +62 -0
  75. django_cfg/apps/payments/views/api_key_views.py +164 -0
  76. django_cfg/apps/payments/views/balance_views.py +75 -0
  77. django_cfg/apps/payments/views/currency_views.py +111 -0
  78. django_cfg/apps/payments/views/payment_views.py +111 -0
  79. django_cfg/apps/payments/views/subscription_views.py +135 -0
  80. django_cfg/apps/payments/views/tariff_views.py +131 -0
  81. django_cfg/apps/support/signals.py +16 -4
  82. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  83. django_cfg/core/config.py +6 -0
  84. django_cfg/models/revolution.py +14 -0
  85. django_cfg/modules/base.py +9 -0
  86. django_cfg/modules/django_email.py +42 -4
  87. django_cfg/modules/django_unfold/dashboard.py +20 -0
  88. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
  89. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
  90. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
  91. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
  92. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,296 @@
1
+ """
2
+ Usage Tracking Middleware.
3
+ Tracks API usage for billing, analytics, and monitoring.
4
+ """
5
+
6
+ import logging
7
+ import json
8
+ from typing import Optional, Dict, Any
9
+ from django.http import HttpRequest, HttpResponse
10
+ from django.utils.deprecation import MiddlewareMixin
11
+ from django.conf import settings
12
+ from django.utils import timezone
13
+ from ..models import APIKey, Subscription, Transaction
14
+ from ..services import RateLimitCache
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class UsageTrackingMiddleware(MiddlewareMixin):
20
+ """
21
+ Middleware for tracking API usage and creating billing records.
22
+
23
+ Features:
24
+ - Request/response logging
25
+ - Usage analytics
26
+ - Billing event creation
27
+ - Performance monitoring
28
+ - Error tracking
29
+ """
30
+
31
+ def __init__(self, get_response=None):
32
+ super().__init__(get_response)
33
+ self.redis_service = RedisService()
34
+
35
+ # Enable/disable usage tracking
36
+ self.enabled = getattr(settings, 'PAYMENTS_USAGE_TRACKING_ENABLED', True)
37
+
38
+ # Paths to track (empty means track all API paths)
39
+ self.tracked_paths = getattr(settings, 'PAYMENTS_TRACKED_PATHS', [])
40
+
41
+ # Paths to exclude from tracking
42
+ self.excluded_paths = getattr(settings, 'PAYMENTS_EXCLUDED_PATHS', [
43
+ '/admin/',
44
+ '/cfg/',
45
+ '/api/v1/api-key/validate/',
46
+ ])
47
+
48
+ # Track request bodies (be careful with sensitive data)
49
+ self.track_request_body = getattr(settings, 'PAYMENTS_TRACK_REQUEST_BODY', False)
50
+
51
+ # Track response bodies (be careful with large responses)
52
+ self.track_response_body = getattr(settings, 'PAYMENTS_TRACK_RESPONSE_BODY', False)
53
+
54
+ def process_request(self, request: HttpRequest) -> None:
55
+ """Process incoming request for usage tracking."""
56
+
57
+ if not self.enabled:
58
+ return
59
+
60
+ # Skip excluded paths
61
+ if self._is_excluded_path(request):
62
+ return
63
+
64
+ # Only track if we have API key
65
+ if not hasattr(request, 'payment_api_key'):
66
+ return
67
+
68
+ # Record request start time
69
+ request._usage_start_time = timezone.now()
70
+
71
+ # Prepare usage data
72
+ request._usage_data = {
73
+ 'api_key_id': request.payment_api_key.id,
74
+ 'user_id': request.payment_api_key.user.id,
75
+ 'method': request.method,
76
+ 'path': request.path,
77
+ 'query_params': dict(request.GET),
78
+ 'user_agent': request.META.get('HTTP_USER_AGENT', ''),
79
+ 'ip_address': self._get_client_ip(request),
80
+ 'start_time': request._usage_start_time,
81
+ }
82
+
83
+ # Add subscription info if available
84
+ if hasattr(request, 'payment_subscription'):
85
+ request._usage_data.update({
86
+ 'subscription_id': request.payment_subscription.id,
87
+ 'endpoint_group_id': request.payment_subscription.endpoint_group.id,
88
+ 'tier': request.payment_subscription.tier,
89
+ })
90
+
91
+ # Track request body if enabled and safe
92
+ if self.track_request_body and self._is_safe_to_track_body(request):
93
+ try:
94
+ request._usage_data['request_body'] = request.body.decode('utf-8')[:1000] # Limit size
95
+ except:
96
+ pass
97
+
98
+ def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
99
+ """Process response for usage tracking."""
100
+
101
+ if not self.enabled or not hasattr(request, '_usage_data'):
102
+ return response
103
+
104
+ try:
105
+ # Calculate response time
106
+ end_time = timezone.now()
107
+ response_time_ms = int((end_time - request._usage_start_time).total_seconds() * 1000)
108
+
109
+ # Update usage data
110
+ usage_data = request._usage_data
111
+ usage_data.update({
112
+ 'end_time': end_time,
113
+ 'response_time_ms': response_time_ms,
114
+ 'status_code': response.status_code,
115
+ 'response_size': len(response.content) if hasattr(response, 'content') else 0,
116
+ })
117
+
118
+ # Track response body if enabled and safe
119
+ if (self.track_response_body and
120
+ self._is_safe_to_track_response(response) and
121
+ response.status_code < 400):
122
+ try:
123
+ content = response.content.decode('utf-8')[:1000] # Limit size
124
+ usage_data['response_body'] = content
125
+ except:
126
+ pass
127
+
128
+ # Track error details for failed requests
129
+ if response.status_code >= 400:
130
+ usage_data['is_error'] = True
131
+ usage_data['error_category'] = self._categorize_error(response.status_code)
132
+ else:
133
+ usage_data['is_error'] = False
134
+
135
+ # Store in Redis for real-time analytics
136
+ self._store_usage_data(usage_data)
137
+
138
+ # Create billing transaction if needed
139
+ if hasattr(request, 'payment_subscription'):
140
+ self._create_billing_transaction(request.payment_subscription, usage_data)
141
+
142
+ # Log for debugging
143
+ logger.info(
144
+ f"API usage tracked - User: {usage_data['user_id']}, "
145
+ f"Path: {usage_data['path']}, "
146
+ f"Status: {usage_data['status_code']}, "
147
+ f"Time: {response_time_ms}ms"
148
+ )
149
+
150
+ except Exception as e:
151
+ logger.error(f"Error in usage tracking: {e}")
152
+
153
+ return response
154
+
155
+ def _is_excluded_path(self, request: HttpRequest) -> bool:
156
+ """Check if path should be excluded from tracking."""
157
+ path = request.path
158
+
159
+ # Check excluded paths
160
+ for excluded in self.excluded_paths:
161
+ if path.startswith(excluded):
162
+ return True
163
+
164
+ # If tracked_paths is specified, only track those
165
+ if self.tracked_paths:
166
+ return not any(path.startswith(tracked) for tracked in self.tracked_paths)
167
+
168
+ return False
169
+
170
+ def _get_client_ip(self, request: HttpRequest) -> Optional[str]:
171
+ """Get client IP address."""
172
+
173
+ # Check for forwarded headers first
174
+ forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
175
+ if forwarded_for:
176
+ return forwarded_for.split(',')[0].strip()
177
+
178
+ # Check for real IP header
179
+ real_ip = request.META.get('HTTP_X_REAL_IP')
180
+ if real_ip:
181
+ return real_ip
182
+
183
+ # Fallback to remote address
184
+ return request.META.get('REMOTE_ADDR')
185
+
186
+ def _is_safe_to_track_body(self, request: HttpRequest) -> bool:
187
+ """Check if it's safe to track request body."""
188
+
189
+ # Don't track large bodies
190
+ content_length = request.META.get('CONTENT_LENGTH')
191
+ if content_length and int(content_length) > 10000: # 10KB limit
192
+ return False
193
+
194
+ # Don't track file uploads
195
+ content_type = request.META.get('CONTENT_TYPE', '')
196
+ if 'multipart/form-data' in content_type:
197
+ return False
198
+
199
+ # Don't track sensitive endpoints
200
+ sensitive_paths = ['/api/v1/api-key/', '/api/v1/payment/']
201
+ path = request.path
202
+ if any(path.startswith(sensitive) for sensitive in sensitive_paths):
203
+ return False
204
+
205
+ return True
206
+
207
+ def _is_safe_to_track_response(self, response: HttpResponse) -> bool:
208
+ """Check if it's safe to track response body."""
209
+
210
+ # Don't track large responses
211
+ if hasattr(response, 'content') and len(response.content) > 10000: # 10KB limit
212
+ return False
213
+
214
+ # Only track JSON responses
215
+ content_type = response.get('Content-Type', '')
216
+ if 'application/json' not in content_type:
217
+ return False
218
+
219
+ return True
220
+
221
+ def _categorize_error(self, status_code: int) -> str:
222
+ """Categorize error by status code."""
223
+
224
+ if 400 <= status_code < 500:
225
+ return 'client_error'
226
+ elif 500 <= status_code < 600:
227
+ return 'server_error'
228
+ else:
229
+ return 'unknown_error'
230
+
231
+ def _store_usage_data(self, usage_data: Dict[str, Any]):
232
+ """Store usage data in Redis for analytics."""
233
+
234
+ try:
235
+ # Store daily usage stats
236
+ date_key = usage_data['start_time'].strftime('%Y-%m-%d')
237
+ user_id = usage_data['user_id']
238
+
239
+ # Increment counters
240
+ self.redis_service.increment_daily_usage(user_id, date_key)
241
+
242
+ if usage_data.get('subscription_id'):
243
+ self.redis_service.increment_subscription_usage(
244
+ usage_data['subscription_id'],
245
+ date_key
246
+ )
247
+
248
+ # Store performance metrics
249
+ if usage_data['response_time_ms'] > 0:
250
+ self.redis_service.record_response_time(
251
+ usage_data['path'],
252
+ usage_data['response_time_ms']
253
+ )
254
+
255
+ # Store error metrics
256
+ if usage_data.get('is_error'):
257
+ self.redis_service.increment_error_count(
258
+ usage_data['path'],
259
+ usage_data['status_code']
260
+ )
261
+
262
+ except Exception as e:
263
+ logger.error(f"Error storing usage data in Redis: {e}")
264
+
265
+ def _create_billing_transaction(self, subscription: Subscription, usage_data: Dict[str, Any]):
266
+ """Create billing transaction for usage-based pricing."""
267
+
268
+ try:
269
+ # Only create transaction for successful requests
270
+ if usage_data.get('is_error'):
271
+ return
272
+
273
+ # Check if this endpoint has usage-based pricing
274
+ # For now, we'll create a small transaction for each API call
275
+ # This could be batched or calculated differently based on business logic
276
+
277
+ cost_per_request = 0.001 # $0.001 per request (example)
278
+
279
+ # Create transaction record
280
+ Transaction.objects.create(
281
+ user=subscription.user,
282
+ subscription=subscription,
283
+ transaction_type='debit',
284
+ amount_usd=-cost_per_request, # Negative for debit
285
+ description=f"API usage: {usage_data['method']} {usage_data['path']}",
286
+ metadata={
287
+ 'api_call_id': f"{usage_data['api_key_id']}_{usage_data['start_time'].timestamp()}",
288
+ 'endpoint': usage_data['path'],
289
+ 'method': usage_data['method'],
290
+ 'response_time_ms': usage_data['response_time_ms'],
291
+ 'status_code': usage_data['status_code'],
292
+ }
293
+ )
294
+
295
+ except Exception as e:
296
+ logger.error(f"Error creating billing transaction: {e}")