django-cfg 1.4.59__py3-none-any.whl → 1.4.61__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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (53) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/ipc/RPC_LOGGING.md +321 -0
  3. django_cfg/apps/ipc/TESTING.md +539 -0
  4. django_cfg/apps/ipc/__init__.py +12 -3
  5. django_cfg/apps/ipc/admin.py +212 -0
  6. django_cfg/apps/ipc/migrations/0001_initial.py +137 -0
  7. django_cfg/apps/ipc/migrations/__init__.py +0 -0
  8. django_cfg/apps/ipc/models.py +221 -0
  9. django_cfg/apps/ipc/serializers/__init__.py +10 -0
  10. django_cfg/apps/ipc/serializers/serializers.py +114 -0
  11. django_cfg/apps/ipc/services/client/client.py +83 -4
  12. django_cfg/apps/ipc/services/logging.py +239 -0
  13. django_cfg/apps/ipc/services/monitor.py +5 -3
  14. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +269 -0
  15. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +259 -0
  16. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +375 -0
  17. django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +22 -0
  18. django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +9 -0
  19. django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +9 -0
  20. django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +23 -0
  21. django_cfg/apps/ipc/templates/django_cfg_ipc/components/stat_cards.html +50 -0
  22. django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +47 -0
  23. django_cfg/apps/ipc/templates/django_cfg_ipc/components/tab_navigation.html +29 -0
  24. django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +184 -0
  25. django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +56 -0
  26. django_cfg/apps/ipc/urls.py +4 -2
  27. django_cfg/apps/ipc/views/__init__.py +7 -2
  28. django_cfg/apps/ipc/views/dashboard.py +1 -1
  29. django_cfg/apps/ipc/views/{viewsets.py → monitoring.py} +17 -11
  30. django_cfg/apps/ipc/views/testing.py +285 -0
  31. django_cfg/modules/django_client/system/generate_mjs_clients.py +1 -1
  32. django_cfg/modules/django_dashboard/sections/widgets.py +209 -0
  33. django_cfg/modules/django_unfold/callbacks/main.py +43 -18
  34. django_cfg/modules/django_unfold/dashboard.py +41 -4
  35. django_cfg/pyproject.toml +1 -1
  36. django_cfg/static/js/api/index.mjs +8 -3
  37. django_cfg/static/js/api/ipc/client.mjs +40 -0
  38. django_cfg/static/js/api/knowbase/client.mjs +309 -0
  39. django_cfg/static/js/api/knowbase/index.mjs +13 -0
  40. django_cfg/static/js/api/payments/client.mjs +46 -1215
  41. django_cfg/static/js/api/types.mjs +164 -337
  42. django_cfg/templates/admin/index.html +8 -0
  43. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +13 -1
  44. django_cfg/templates/admin/sections/widgets_section.html +129 -0
  45. django_cfg/templates/admin/snippets/tabs/widgets_tab.html +38 -0
  46. {django_cfg-1.4.59.dist-info → django_cfg-1.4.61.dist-info}/METADATA +1 -1
  47. {django_cfg-1.4.59.dist-info → django_cfg-1.4.61.dist-info}/RECORD +52 -28
  48. django_cfg/apps/ipc/templates/django_cfg_ipc/dashboard.html +0 -202
  49. /django_cfg/apps/ipc/static/django_cfg_ipc/js/{dashboard.mjs → dashboard.mjs.old} +0 -0
  50. /django_cfg/apps/ipc/templates/django_cfg_ipc/{base.html → layout/base.html} +0 -0
  51. {django_cfg-1.4.59.dist-info → django_cfg-1.4.61.dist-info}/WHEEL +0 -0
  52. {django_cfg-1.4.59.dist-info → django_cfg-1.4.61.dist-info}/entry_points.txt +0 -0
  53. {django_cfg-1.4.59.dist-info → django_cfg-1.4.61.dist-info}/licenses/LICENSE +0 -0
@@ -61,6 +61,7 @@ class OverviewStatsSerializer(serializers.Serializer):
61
61
  help_text="List of active RPC methods"
62
62
  )
63
63
  top_method = serializers.CharField(
64
+ required=False,
64
65
  allow_null=True,
65
66
  help_text="Most frequently called method"
66
67
  )
@@ -227,3 +228,116 @@ class MethodStatsSerializer(serializers.Serializer):
227
228
  total_calls = serializers.IntegerField(
228
229
  help_text="Total calls across all methods"
229
230
  )
231
+
232
+
233
+ # Testing Serializers
234
+
235
+ class TestRPCRequestSerializer(serializers.Serializer):
236
+ """Serializer for test RPC request input."""
237
+
238
+ method = serializers.CharField(
239
+ help_text="RPC method to call"
240
+ )
241
+ params = serializers.JSONField(
242
+ help_text="Parameters for the RPC call"
243
+ )
244
+ timeout = serializers.IntegerField(
245
+ default=10,
246
+ min_value=1,
247
+ max_value=60,
248
+ help_text="Timeout in seconds"
249
+ )
250
+
251
+
252
+ class TestRPCResponseSerializer(serializers.Serializer):
253
+ """Serializer for test RPC response."""
254
+
255
+ success = serializers.BooleanField(
256
+ help_text="Whether the call was successful"
257
+ )
258
+ duration_ms = serializers.FloatField(
259
+ help_text="Call duration in milliseconds"
260
+ )
261
+ response = serializers.JSONField(
262
+ allow_null=True,
263
+ required=False,
264
+ help_text="Response data from RPC call"
265
+ )
266
+ error = serializers.CharField(
267
+ allow_null=True,
268
+ required=False,
269
+ help_text="Error message if failed"
270
+ )
271
+ correlation_id = serializers.CharField(
272
+ help_text="Correlation ID for tracking"
273
+ )
274
+
275
+
276
+ class LoadTestRequestSerializer(serializers.Serializer):
277
+ """Serializer for load test request input."""
278
+
279
+ method = serializers.CharField(
280
+ help_text="RPC method to test"
281
+ )
282
+ total_requests = serializers.IntegerField(
283
+ min_value=1,
284
+ max_value=10000,
285
+ help_text="Total number of requests to send"
286
+ )
287
+ concurrency = serializers.IntegerField(
288
+ min_value=1,
289
+ max_value=100,
290
+ help_text="Number of concurrent requests"
291
+ )
292
+ params = serializers.JSONField(
293
+ required=False,
294
+ default=dict,
295
+ help_text="Parameters template for RPC calls"
296
+ )
297
+
298
+
299
+ class LoadTestResponseSerializer(serializers.Serializer):
300
+ """Serializer for load test response."""
301
+
302
+ test_id = serializers.CharField(
303
+ help_text="Unique test ID"
304
+ )
305
+ started = serializers.BooleanField(
306
+ help_text="Whether test was started successfully"
307
+ )
308
+ message = serializers.CharField(
309
+ help_text="Status message"
310
+ )
311
+
312
+
313
+ class LoadTestStatusSerializer(serializers.Serializer):
314
+ """Serializer for load test status."""
315
+
316
+ test_id = serializers.CharField(
317
+ allow_null=True,
318
+ help_text="Unique test ID"
319
+ )
320
+ running = serializers.BooleanField(
321
+ help_text="Whether test is currently running"
322
+ )
323
+ progress = serializers.IntegerField(
324
+ help_text="Number of completed requests"
325
+ )
326
+ total = serializers.IntegerField(
327
+ help_text="Total number of requests"
328
+ )
329
+ success_count = serializers.IntegerField(
330
+ help_text="Number of successful requests"
331
+ )
332
+ failed_count = serializers.IntegerField(
333
+ help_text="Number of failed requests"
334
+ )
335
+ avg_duration_ms = serializers.FloatField(
336
+ help_text="Average duration in milliseconds"
337
+ )
338
+ elapsed_time = serializers.FloatField(
339
+ help_text="Total elapsed time in seconds"
340
+ )
341
+ rps = serializers.FloatField(
342
+ help_text="Requests per second"
343
+ )
@@ -10,11 +10,11 @@ Works with or without django-cfg-rpc installed:
10
10
  """
11
11
 
12
12
  import json
13
- import logging
14
13
  from typing import Any, Dict, Optional, Type, TypeVar
15
14
  from uuid import uuid4
16
15
 
17
16
  import redis
17
+ from django_cfg.modules.django_logging import get_logger
18
18
 
19
19
  from .exceptions import (
20
20
  HAS_DJANGO_CFG_RPC,
@@ -24,7 +24,7 @@ from .exceptions import (
24
24
  RPCTimeoutError,
25
25
  )
26
26
 
27
- logger = logging.getLogger(__name__)
27
+ logger = get_logger("ipc.client")
28
28
 
29
29
  # Try to import Pydantic from django-cfg-rpc
30
30
  if HAS_DJANGO_CFG_RPC:
@@ -178,6 +178,9 @@ class DjangoCfgRPCClient:
178
178
  params: Any,
179
179
  result_model: Optional[Type[TResult]] = None,
180
180
  timeout: Optional[int] = None,
181
+ user: Optional[Any] = None,
182
+ caller_ip: Optional[str] = None,
183
+ user_agent: Optional[str] = None,
181
184
  ) -> Any:
182
185
  """
183
186
  Make synchronous RPC call to django-cfg-rpc server.
@@ -187,6 +190,9 @@ class DjangoCfgRPCClient:
187
190
  params: Pydantic model or dict with parameters
188
191
  result_model: Expected result model class (optional)
189
192
  timeout: Optional timeout override (seconds)
193
+ user: Django User instance for logging (optional)
194
+ caller_ip: IP address for logging (optional)
195
+ user_agent: User agent for logging (optional)
190
196
 
191
197
  Returns:
192
198
  Pydantic result model instance (if result_model provided) or dict
@@ -203,10 +209,13 @@ class DjangoCfgRPCClient:
203
209
  ... params=NotificationRequest(user_id="123", type="info",
204
210
  ... title="Hello", message="World"),
205
211
  ... result_model=NotificationResponse,
206
- ... timeout=10
212
+ ... timeout=10,
213
+ ... user=request.user
207
214
  ... )
208
215
  >>> print(result.delivered) # True/False
209
216
  """
217
+ import time
218
+
210
219
  timeout = timeout or self.default_timeout
211
220
 
212
221
  # Generate correlation ID
@@ -223,11 +232,13 @@ class DjangoCfgRPCClient:
223
232
  else:
224
233
  params_json = json.dumps({"data": params})
225
234
 
235
+ params_dict = json.loads(params_json)
236
+
226
237
  # Build RPC request payload
227
238
  request_payload = {
228
239
  "type": "rpc",
229
240
  "method": method,
230
- "params": json.loads(params_json), # Embedded as dict
241
+ "params": params_dict, # Embedded as dict
231
242
  "correlation_id": cid,
232
243
  "reply_to": reply_key, # Redis List key for response
233
244
  "timeout": timeout,
@@ -236,6 +247,25 @@ class DjangoCfgRPCClient:
236
247
  if self.log_calls:
237
248
  logger.debug(f"RPC call: {method} (cid={cid})")
238
249
 
250
+ # Start timing for logging
251
+ start_time = time.time()
252
+ log_entry = None
253
+
254
+ # Create log entry if logging enabled
255
+ try:
256
+ from ..logging import RPCLogger
257
+ log_entry = RPCLogger.create_log(
258
+ correlation_id=cid,
259
+ method=method,
260
+ params=params_dict,
261
+ user=user,
262
+ caller_ip=caller_ip,
263
+ user_agent=user_agent,
264
+ )
265
+ except Exception as e:
266
+ # Don't fail RPC call if logging fails
267
+ logger.warning(f"Failed to create RPC log: {e}")
268
+
239
269
  try:
240
270
  # Send request to Redis Stream
241
271
  message_id = self._redis.xadd(
@@ -253,7 +283,17 @@ class DjangoCfgRPCClient:
253
283
 
254
284
  if response_data is None:
255
285
  # Timeout occurred
286
+ duration_ms = int((time.time() - start_time) * 1000)
256
287
  logger.warning(f"RPC timeout: {method} (cid={cid}, timeout={timeout}s)")
288
+
289
+ # Log timeout
290
+ if log_entry:
291
+ try:
292
+ from ..logging import RPCLogger
293
+ RPCLogger.mark_timeout(log_entry, timeout)
294
+ except Exception:
295
+ pass
296
+
257
297
  raise RPCTimeoutError(
258
298
  f"RPC call '{method}' timed out after {timeout}s",
259
299
  method=method,
@@ -272,18 +312,57 @@ class DjangoCfgRPCClient:
272
312
  # Check response type
273
313
  if response_dict.get("type") == "error":
274
314
  # Error response
315
+ duration_ms = int((time.time() - start_time) * 1000)
275
316
  error_data = response_dict.get("error", {})
317
+
318
+ # Log error
319
+ if log_entry:
320
+ try:
321
+ from ..logging import RPCLogger
322
+ RPCLogger.mark_failed(
323
+ log_entry,
324
+ error_data.get("code", "unknown"),
325
+ error_data.get("message", "Unknown error"),
326
+ duration_ms
327
+ )
328
+ except Exception:
329
+ pass
330
+
276
331
  raise RPCRemoteError(error_data)
277
332
 
278
333
  # Extract result
279
334
  result_data = response_dict.get("result")
280
335
 
281
336
  if result_data is None:
337
+ duration_ms = int((time.time() - start_time) * 1000)
338
+
339
+ # Log error
340
+ if log_entry:
341
+ try:
342
+ from ..logging import RPCLogger
343
+ RPCLogger.mark_failed(
344
+ log_entry,
345
+ "internal_error",
346
+ "Response has no result field",
347
+ duration_ms
348
+ )
349
+ except Exception:
350
+ pass
351
+
282
352
  raise RPCRemoteError({
283
353
  "code": "internal_error",
284
354
  "message": "Response has no result field",
285
355
  })
286
356
 
357
+ # Success - log it
358
+ duration_ms = int((time.time() - start_time) * 1000)
359
+ if log_entry:
360
+ try:
361
+ from ..logging import RPCLogger
362
+ RPCLogger.mark_success(log_entry, result_data, duration_ms)
363
+ except Exception:
364
+ pass
365
+
287
366
  # Deserialize result if model provided
288
367
  if result_model and HAS_DJANGO_CFG_RPC:
289
368
  try:
@@ -0,0 +1,239 @@
1
+ """
2
+ RPC Logging helper for tracking RPC calls.
3
+
4
+ Provides async-safe logging of RPC calls to database.
5
+ """
6
+
7
+ import time
8
+ from typing import Any, Dict, Optional
9
+ from django.conf import settings
10
+ from django_cfg.modules.django_logging import get_logger
11
+
12
+ logger = get_logger("ipc.rpc")
13
+
14
+
15
+ class RPCLogger:
16
+ """
17
+ Helper class for logging RPC calls to database.
18
+
19
+ Usage:
20
+ >>> log_entry = RPCLogger.create_log(
21
+ ... correlation_id="abc123",
22
+ ... method="send_notification",
23
+ ... params={"user_id": "123"},
24
+ ... user=request.user if authenticated else None
25
+ ... )
26
+ >>> # ... make RPC call ...
27
+ >>> RPCLogger.mark_success(log_entry, response_data, duration_ms=150)
28
+ """
29
+
30
+ @staticmethod
31
+ def is_logging_enabled() -> bool:
32
+ """
33
+ Check if RPC logging is enabled in settings.
34
+
35
+ Returns:
36
+ bool: True if logging is enabled
37
+ """
38
+ # Check if IPC app is installed
39
+ if 'django_cfg.apps.ipc' not in settings.INSTALLED_APPS:
40
+ return False
41
+
42
+ # Check if logging is explicitly disabled
43
+ if hasattr(settings, 'DJANGO_CFG_RPC'):
44
+ return settings.DJANGO_CFG_RPC.get('ENABLE_LOGGING', True)
45
+
46
+ return True
47
+
48
+ @staticmethod
49
+ def create_log(
50
+ correlation_id: str,
51
+ method: str,
52
+ params: Dict[str, Any],
53
+ user: Optional[Any] = None,
54
+ caller_ip: Optional[str] = None,
55
+ user_agent: Optional[str] = None,
56
+ ):
57
+ """
58
+ Create RPC log entry in pending state.
59
+
60
+ Args:
61
+ correlation_id: UUID correlation ID from RPC request
62
+ method: RPC method name
63
+ params: Parameters sent to RPC method
64
+ user: Django User instance (optional)
65
+ caller_ip: IP address of caller (optional)
66
+ user_agent: User agent string (optional)
67
+
68
+ Returns:
69
+ RPCLog instance or None if logging disabled
70
+ """
71
+ if not RPCLogger.is_logging_enabled():
72
+ return None
73
+
74
+ try:
75
+ from ..models import RPCLog
76
+
77
+ log_entry = RPCLog.objects.create(
78
+ correlation_id=correlation_id,
79
+ method=method,
80
+ params=params,
81
+ user=user,
82
+ caller_ip=caller_ip,
83
+ user_agent=user_agent,
84
+ status=RPCLog.StatusChoices.PENDING
85
+ )
86
+ return log_entry
87
+
88
+ except Exception as e:
89
+ logger.error(f"Failed to create RPC log: {e}", exc_info=True)
90
+ return None
91
+
92
+ @staticmethod
93
+ def mark_success(log_entry, response_data: Dict[str, Any], duration_ms: Optional[int] = None):
94
+ """
95
+ Mark RPC log as successful.
96
+
97
+ Args:
98
+ log_entry: RPCLog instance
99
+ response_data: Response data from RPC call
100
+ duration_ms: Duration in milliseconds (optional)
101
+ """
102
+ if log_entry is None:
103
+ return
104
+
105
+ try:
106
+ log_entry.mark_success(response_data, duration_ms)
107
+ except Exception as e:
108
+ logger.error(f"Failed to mark RPC log as success: {e}", exc_info=True)
109
+
110
+ @staticmethod
111
+ def mark_failed(log_entry, error_code: str, error_message: str, duration_ms: Optional[int] = None):
112
+ """
113
+ Mark RPC log as failed.
114
+
115
+ Args:
116
+ log_entry: RPCLog instance
117
+ error_code: Error code
118
+ error_message: Error message
119
+ duration_ms: Duration in milliseconds (optional)
120
+ """
121
+ if log_entry is None:
122
+ return
123
+
124
+ try:
125
+ log_entry.mark_failed(error_code, error_message, duration_ms)
126
+ except Exception as e:
127
+ logger.error(f"Failed to mark RPC log as failed: {e}", exc_info=True)
128
+
129
+ @staticmethod
130
+ def mark_timeout(log_entry, timeout_seconds: int):
131
+ """
132
+ Mark RPC log as timed out.
133
+
134
+ Args:
135
+ log_entry: RPCLog instance
136
+ timeout_seconds: Timeout duration in seconds
137
+ """
138
+ if log_entry is None:
139
+ return
140
+
141
+ try:
142
+ log_entry.mark_timeout(timeout_seconds)
143
+ except Exception as e:
144
+ logger.error(f"Failed to mark RPC log as timeout: {e}", exc_info=True)
145
+
146
+
147
+ class RPCLogContext:
148
+ """
149
+ Context manager for automatically logging RPC calls.
150
+
151
+ Usage:
152
+ >>> with RPCLogContext(
153
+ ... correlation_id="abc123",
154
+ ... method="send_notification",
155
+ ... params={"user_id": "123"}
156
+ ... ) as log_ctx:
157
+ ... result = rpc.call(...)
158
+ ... log_ctx.set_response(result)
159
+ """
160
+
161
+ def __init__(
162
+ self,
163
+ correlation_id: str,
164
+ method: str,
165
+ params: Dict[str, Any],
166
+ user: Optional[Any] = None,
167
+ caller_ip: Optional[str] = None,
168
+ user_agent: Optional[str] = None,
169
+ ):
170
+ """
171
+ Initialize RPC log context.
172
+
173
+ Args:
174
+ correlation_id: UUID correlation ID
175
+ method: RPC method name
176
+ params: Parameters for RPC call
177
+ user: Django User instance (optional)
178
+ caller_ip: IP address (optional)
179
+ user_agent: User agent string (optional)
180
+ """
181
+ self.correlation_id = correlation_id
182
+ self.method = method
183
+ self.params = params
184
+ self.user = user
185
+ self.caller_ip = caller_ip
186
+ self.user_agent = user_agent
187
+ self.log_entry = None
188
+ self.start_time = None
189
+ self.response = None
190
+
191
+ def __enter__(self):
192
+ """Start timing and create log entry."""
193
+ self.start_time = time.time()
194
+ self.log_entry = RPCLogger.create_log(
195
+ correlation_id=self.correlation_id,
196
+ method=self.method,
197
+ params=self.params,
198
+ user=self.user,
199
+ caller_ip=self.caller_ip,
200
+ user_agent=self.user_agent,
201
+ )
202
+ return self
203
+
204
+ def __exit__(self, exc_type, exc_val, exc_tb):
205
+ """Mark log as success or failed based on exception."""
206
+ if self.log_entry is None:
207
+ return False
208
+
209
+ duration_ms = int((time.time() - self.start_time) * 1000) if self.start_time else None
210
+
211
+ if exc_type is None:
212
+ # Success - use response if set
213
+ RPCLogger.mark_success(self.log_entry, self.response or {}, duration_ms)
214
+ else:
215
+ # Failed - extract error details
216
+ error_code = exc_type.__name__ if exc_type else 'unknown'
217
+ error_message = str(exc_val) if exc_val else 'Unknown error'
218
+
219
+ # Check if it's a timeout
220
+ if 'timeout' in error_code.lower():
221
+ timeout_seconds = duration_ms // 1000 if duration_ms else 30
222
+ RPCLogger.mark_timeout(self.log_entry, timeout_seconds)
223
+ else:
224
+ RPCLogger.mark_failed(self.log_entry, error_code, error_message, duration_ms)
225
+
226
+ # Don't suppress exceptions
227
+ return False
228
+
229
+ def set_response(self, response: Dict[str, Any]):
230
+ """
231
+ Set response data for successful call.
232
+
233
+ Args:
234
+ response: Response data from RPC call
235
+ """
236
+ self.response = response
237
+
238
+
239
+ __all__ = ['RPCLogger', 'RPCLogContext']
@@ -5,12 +5,13 @@ Reads RPC metrics from Redis DB 2 and provides statistics.
5
5
  """
6
6
 
7
7
  import json
8
- import logging
9
8
  from collections import defaultdict
10
9
  from datetime import datetime, timedelta
11
10
  from typing import Any, Dict, List
12
11
 
13
- logger = logging.getLogger(__name__)
12
+ from django_cfg.modules.django_logging import get_logger
13
+
14
+ logger = get_logger("ipc.monitor")
14
15
 
15
16
  # Cache timeout in seconds
16
17
  CACHE_TIMEOUT = 3 # 3 seconds to reduce Redis load
@@ -117,6 +118,7 @@ class RPCMonitor:
117
118
  'redis_connected': True,
118
119
  'total_requests_today': 0,
119
120
  'active_methods': [],
121
+ 'top_method': None,
120
122
  'avg_response_time_ms': 0,
121
123
  'success_rate': 100.0,
122
124
  'timestamp': datetime.now().isoformat(),
@@ -354,7 +356,7 @@ class RPCMonitor:
354
356
  stream_key = stream_key or self.config.request_stream
355
357
 
356
358
  # Validate stream_key (security: prevent Redis key injection)
357
- ALLOWED_STREAMS = ['stream:requests', 'stream:responses']
359
+ ALLOWED_STREAMS = ['stream:requests', 'stream:responses', 'stream:rpc_requests', 'stream:rpc_responses']
358
360
  if stream_key not in ALLOWED_STREAMS:
359
361
  logger.warning(f"Invalid stream key attempted: {stream_key}")
360
362
  raise ValueError(f"Stream key not allowed: {stream_key}")