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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/newsletter/signals.py +9 -8
- django_cfg/apps/payments/__init__.py +8 -0
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/apps.py +22 -0
- django_cfg/apps/payments/config/__init__.py +87 -0
- django_cfg/apps/payments/config/module.py +162 -0
- django_cfg/apps/payments/config/providers.py +93 -0
- django_cfg/apps/payments/config/settings.py +136 -0
- django_cfg/apps/payments/config/utils.py +198 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/managers/__init__.py +22 -0
- django_cfg/apps/payments/managers/api_key_manager.py +35 -0
- django_cfg/apps/payments/managers/balance_manager.py +361 -0
- django_cfg/apps/payments/managers/currency_manager.py +32 -0
- django_cfg/apps/payments/managers/payment_manager.py +44 -0
- django_cfg/apps/payments/managers/subscription_manager.py +37 -0
- django_cfg/apps/payments/managers/tariff_manager.py +29 -0
- django_cfg/apps/payments/middleware/__init__.py +13 -0
- django_cfg/apps/payments/middleware/api_access.py +261 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +1003 -0
- django_cfg/apps/payments/migrations/__init__.py +1 -0
- django_cfg/apps/payments/models/__init__.py +67 -0
- django_cfg/apps/payments/models/api_keys.py +96 -0
- django_cfg/apps/payments/models/balance.py +209 -0
- django_cfg/apps/payments/models/base.py +30 -0
- django_cfg/apps/payments/models/currencies.py +138 -0
- django_cfg/apps/payments/models/events.py +73 -0
- django_cfg/apps/payments/models/payments.py +301 -0
- django_cfg/apps/payments/models/subscriptions.py +270 -0
- django_cfg/apps/payments/models/tariffs.py +102 -0
- django_cfg/apps/payments/serializers/__init__.py +56 -0
- django_cfg/apps/payments/serializers/api_keys.py +51 -0
- django_cfg/apps/payments/serializers/balance.py +59 -0
- django_cfg/apps/payments/serializers/currencies.py +55 -0
- django_cfg/apps/payments/serializers/payments.py +62 -0
- django_cfg/apps/payments/serializers/subscriptions.py +71 -0
- django_cfg/apps/payments/serializers/tariffs.py +56 -0
- django_cfg/apps/payments/services/__init__.py +65 -0
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +449 -0
- django_cfg/apps/payments/services/core/payment_service.py +393 -0
- django_cfg/apps/payments/services/core/subscription_service.py +616 -0
- django_cfg/apps/payments/services/internal_types.py +266 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/providers/__init__.py +19 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +99 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +150 -0
- django_cfg/apps/payments/signals/payment_signals.py +127 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/urls.py +78 -0
- django_cfg/apps/payments/utils/__init__.py +42 -0
- django_cfg/apps/payments/utils/config_utils.py +243 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/__init__.py +62 -0
- django_cfg/apps/payments/views/api_key_views.py +164 -0
- django_cfg/apps/payments/views/balance_views.py +75 -0
- django_cfg/apps/payments/views/currency_views.py +111 -0
- django_cfg/apps/payments/views/payment_views.py +111 -0
- django_cfg/apps/payments/views/subscription_views.py +135 -0
- django_cfg/apps/payments/views/tariff_views.py +131 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/core/config.py +6 -0
- django_cfg/models/revolution.py +14 -0
- django_cfg/modules/base.py +9 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
- {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}")
|