sf-veritas 0.10.3__cp39-cp39-manylinux_2_28_x86_64.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 sf-veritas might be problematic. Click here for more details.

Files changed (132) hide show
  1. sf_veritas/__init__.py +20 -0
  2. sf_veritas/_sffastlog.c +889 -0
  3. sf_veritas/_sffastlog.cpython-39-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-39-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-39-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-39-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-39-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-39-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-39-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-39-x86_64-linux-gnu.so +0 -0
  18. sf_veritas/_sfteepreload.c +5167 -0
  19. sf_veritas/app_config.py +49 -0
  20. sf_veritas/cli.py +336 -0
  21. sf_veritas/constants.py +10 -0
  22. sf_veritas/custom_excepthook.py +304 -0
  23. sf_veritas/custom_log_handler.py +129 -0
  24. sf_veritas/custom_output_wrapper.py +144 -0
  25. sf_veritas/custom_print.py +146 -0
  26. sf_veritas/django_app.py +5 -0
  27. sf_veritas/env_vars.py +186 -0
  28. sf_veritas/exception_handling_middleware.py +18 -0
  29. sf_veritas/exception_metaclass.py +69 -0
  30. sf_veritas/fast_frame_info.py +116 -0
  31. sf_veritas/fast_network_hop.py +293 -0
  32. sf_veritas/frame_tools.py +112 -0
  33. sf_veritas/funcspan_config_loader.py +556 -0
  34. sf_veritas/function_span_profiler.py +1174 -0
  35. sf_veritas/import_hook.py +62 -0
  36. sf_veritas/infra_details/__init__.py +3 -0
  37. sf_veritas/infra_details/get_infra_details.py +24 -0
  38. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  39. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  40. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  41. sf_veritas/infra_details/running_on/__init__.py +17 -0
  42. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  43. sf_veritas/interceptors.py +497 -0
  44. sf_veritas/libsfnettee.so +0 -0
  45. sf_veritas/local_env_detect.py +118 -0
  46. sf_veritas/package_metadata.py +6 -0
  47. sf_veritas/patches/__init__.py +0 -0
  48. sf_veritas/patches/concurrent_futures.py +19 -0
  49. sf_veritas/patches/constants.py +1 -0
  50. sf_veritas/patches/exceptions.py +82 -0
  51. sf_veritas/patches/multiprocessing.py +32 -0
  52. sf_veritas/patches/network_libraries/__init__.py +76 -0
  53. sf_veritas/patches/network_libraries/aiohttp.py +281 -0
  54. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  55. sf_veritas/patches/network_libraries/http_client.py +419 -0
  56. sf_veritas/patches/network_libraries/httpcore.py +515 -0
  57. sf_veritas/patches/network_libraries/httplib2.py +204 -0
  58. sf_veritas/patches/network_libraries/httpx.py +515 -0
  59. sf_veritas/patches/network_libraries/niquests.py +211 -0
  60. sf_veritas/patches/network_libraries/pycurl.py +385 -0
  61. sf_veritas/patches/network_libraries/requests.py +633 -0
  62. sf_veritas/patches/network_libraries/tornado.py +341 -0
  63. sf_veritas/patches/network_libraries/treq.py +270 -0
  64. sf_veritas/patches/network_libraries/urllib_request.py +468 -0
  65. sf_veritas/patches/network_libraries/utils.py +398 -0
  66. sf_veritas/patches/os.py +17 -0
  67. sf_veritas/patches/threading.py +218 -0
  68. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  69. sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
  70. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
  71. sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
  72. sf_veritas/patches/web_frameworks/bottle.py +502 -0
  73. sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
  74. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  75. sf_veritas/patches/web_frameworks/django.py +944 -0
  76. sf_veritas/patches/web_frameworks/eve.py +395 -0
  77. sf_veritas/patches/web_frameworks/falcon.py +926 -0
  78. sf_veritas/patches/web_frameworks/fastapi.py +724 -0
  79. sf_veritas/patches/web_frameworks/flask.py +520 -0
  80. sf_veritas/patches/web_frameworks/klein.py +501 -0
  81. sf_veritas/patches/web_frameworks/litestar.py +551 -0
  82. sf_veritas/patches/web_frameworks/pyramid.py +428 -0
  83. sf_veritas/patches/web_frameworks/quart.py +824 -0
  84. sf_veritas/patches/web_frameworks/robyn.py +697 -0
  85. sf_veritas/patches/web_frameworks/sanic.py +857 -0
  86. sf_veritas/patches/web_frameworks/starlette.py +723 -0
  87. sf_veritas/patches/web_frameworks/strawberry.py +813 -0
  88. sf_veritas/patches/web_frameworks/tornado.py +481 -0
  89. sf_veritas/patches/web_frameworks/utils.py +91 -0
  90. sf_veritas/print_override.py +13 -0
  91. sf_veritas/regular_data_transmitter.py +409 -0
  92. sf_veritas/request_interceptor.py +401 -0
  93. sf_veritas/request_utils.py +550 -0
  94. sf_veritas/server_status.py +1 -0
  95. sf_veritas/shutdown_flag.py +11 -0
  96. sf_veritas/subprocess_startup.py +3 -0
  97. sf_veritas/test_cli.py +145 -0
  98. sf_veritas/thread_local.py +970 -0
  99. sf_veritas/timeutil.py +114 -0
  100. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  101. sf_veritas/transmitter.py +132 -0
  102. sf_veritas/types.py +47 -0
  103. sf_veritas/unified_interceptor.py +1580 -0
  104. sf_veritas/utils.py +39 -0
  105. sf_veritas-0.10.3.dist-info/METADATA +97 -0
  106. sf_veritas-0.10.3.dist-info/RECORD +132 -0
  107. sf_veritas-0.10.3.dist-info/WHEEL +5 -0
  108. sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
  109. sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
  110. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  111. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  112. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  113. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  114. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  115. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  116. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  117. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  118. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  119. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  120. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  121. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  122. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  123. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  124. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  125. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  126. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  127. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  128. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  129. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  130. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  131. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  132. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,944 @@
1
+ import asyncio
2
+ import inspect
3
+ import sys
4
+ from typing import List, Optional
5
+
6
+ from ... import _sffuncspan_config
7
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
8
+ from .utils import _is_user_code, _unwrap_user_func, should_skip_route
9
+
10
+ try:
11
+ from django.utils.deprecation import MiddlewareMixin
12
+ except ImportError:
13
+ MiddlewareMixin = object # fallback for non-Django environments
14
+
15
+ import traceback
16
+
17
+ from ... import app_config
18
+ from ...constants import (
19
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
20
+ SAILFISH_TRACING_HEADER,
21
+ SAILFISH_TRACING_HEADER_BYTES,
22
+ )
23
+ from ...custom_excepthook import custom_excepthook
24
+ from ...env_vars import (
25
+ PRINT_CONFIGURATION_STATUSES,
26
+ SF_DEBUG,
27
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
28
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
29
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
30
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
31
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
32
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
33
+ )
34
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
35
+ from ...thread_local import (
36
+ clear_c_tls_parent_trace_id,
37
+ clear_current_request_path,
38
+ clear_outbound_header_base,
39
+ clear_trace_id,
40
+ generate_new_trace_id,
41
+ get_or_set_sf_trace_id,
42
+ get_sf_trace_id,
43
+ set_current_request_path,
44
+ set_outbound_header_base,
45
+ )
46
+
47
+ # Registry mapping view function id → endpoint_id (for fast C path)
48
+ _ENDPOINT_REGISTRY = {}
49
+
50
+ # Module-level variable for routes to skip (set by patch_django_middleware)
51
+ _ROUTES_TO_SKIP = []
52
+
53
+
54
+ def find_and_modify_output_wrapper():
55
+ """
56
+ Monkey-patch Django's OutputWrapper to always use the current sys.stdout/stderr
57
+ instead of storing a reference at init time. This ensures Django management
58
+ commands (like migrate) output is captured by our UnifiedInterceptor even if
59
+ OutputWrapper was instantiated before setup_interceptors() ran.
60
+ """
61
+ if PRINT_CONFIGURATION_STATUSES:
62
+ print("find_and_modify_output_wrapper", log=False)
63
+
64
+ try:
65
+ from django.core.management.base import OutputWrapper
66
+ except ImportError:
67
+ if PRINT_CONFIGURATION_STATUSES:
68
+ print("Django not found; skipping OutputWrapper patch", log=False)
69
+ return
70
+
71
+ # Check if already patched (idempotent)
72
+ if hasattr(OutputWrapper, "_sf_patched"):
73
+ if PRINT_CONFIGURATION_STATUSES:
74
+ print("OutputWrapper already patched; skipping", log=False)
75
+ return
76
+
77
+ # Save original methods
78
+ _original_init = OutputWrapper.__init__
79
+ _original_write = OutputWrapper.write
80
+
81
+ def patched_init(self, out, ending="\n"):
82
+ """Patched __init__ that tracks if this wrapper is wrapping stdout/stderr."""
83
+ # Call original init
84
+ _original_init(self, out, ending)
85
+ # Track if this wrapper is for stdout or stderr (so we can redirect to current stream)
86
+ self._sf_is_stdout = out is sys.stdout or out is sys.__stdout__
87
+ self._sf_is_stderr = out is sys.stderr or out is sys.__stderr__
88
+ if SF_DEBUG and app_config._interceptors_initialized:
89
+ print(
90
+ f"[Django OutputWrapper] Created: stdout={self._sf_is_stdout}, stderr={self._sf_is_stderr}",
91
+ log=False,
92
+ )
93
+
94
+ def patched_write(self, msg="", style_func=None, ending=None):
95
+ """
96
+ Patched write that uses CURRENT sys.stdout/stderr instead of the stored reference.
97
+ This ensures our UnifiedInterceptor captures Django output.
98
+ """
99
+ # If this wrapper was created for stdout, redirect to CURRENT sys.stdout
100
+ if getattr(self, "_sf_is_stdout", False):
101
+ original_out = self._out
102
+ self._out = sys.stdout
103
+ try:
104
+ return _original_write(self, msg, style_func, ending)
105
+ finally:
106
+ self._out = original_out
107
+
108
+ # If this wrapper was created for stderr, redirect to CURRENT sys.stderr
109
+ elif getattr(self, "_sf_is_stderr", False):
110
+ original_out = self._out
111
+ self._out = sys.stderr
112
+ try:
113
+ return _original_write(self, msg, style_func, ending)
114
+ finally:
115
+ self._out = original_out
116
+
117
+ # Otherwise use original behavior
118
+ return _original_write(self, msg, style_func, ending)
119
+
120
+ # Apply patches
121
+ OutputWrapper.__init__ = patched_init
122
+ OutputWrapper.write = patched_write
123
+ OutputWrapper._sf_patched = True
124
+
125
+ if PRINT_CONFIGURATION_STATUSES:
126
+ print("find_and_modify_output_wrapper...DONE (monkey-patched)", log=False)
127
+
128
+
129
+ class SailfishMiddleware(MiddlewareMixin):
130
+ """
131
+ • process_request – capture inbound SAILFISH_TRACING_HEADER header.
132
+ • process_view – emit one NetworkHop per view (skip Strawberry).
133
+ • __call__ override – last-chance catcher for uncaught exceptions.
134
+ • got_request_exception signal – main hook for 500-level errors.
135
+ • process_exception – fallback for view-raised exceptions.
136
+ """
137
+
138
+ # ------------------------------------------------------------------ #
139
+ # 0 | Signal registration (called once at server start-up)
140
+ # ------------------------------------------------------------------ #
141
+ def __init__(self, get_response):
142
+ super().__init__(get_response)
143
+
144
+ # Attach to Django's global exception signal so we ALWAYS
145
+ # see real exceptions that become HTTP-500 responses.
146
+ from django.core.signals import got_request_exception
147
+
148
+ got_request_exception.disconnect( # avoid dupes on reload
149
+ self._on_exception_signal, dispatch_uid="sf_veritas_signal"
150
+ )
151
+ got_request_exception.connect(
152
+ self._on_exception_signal,
153
+ weak=False,
154
+ dispatch_uid="sf_veritas_signal",
155
+ )
156
+
157
+ # ------------------------------------------------------------------ #
158
+ # 1 | Signal handler ← FIXED
159
+ # ------------------------------------------------------------------ #
160
+ def _on_exception_signal(self, sender, request, **kwargs):
161
+ """
162
+ Handle django.core.signals.got_request_exception.
163
+
164
+ The signal doesn't pass the exception object; per Django's own
165
+ implementation (and Sentry's approach) we fetch it from
166
+ sys.exc_info().
167
+ """
168
+
169
+ exc_type, exc_value, exc_tb = sys.exc_info()
170
+
171
+ if SF_DEBUG and app_config._interceptors_initialized:
172
+ print(
173
+ f"[[SailfishMiddleware._on_exception_signal]] "
174
+ f"exc_value={exc_value!r}",
175
+ log=False,
176
+ )
177
+
178
+ if exc_value:
179
+ custom_excepthook(exc_type, exc_value, exc_tb)
180
+
181
+ # ------------------------------------------------------------------ #
182
+ # 2 | Last-chance wrapper (rarely triggered in WSGI but free)
183
+ # ------------------------------------------------------------------ #
184
+ def __call__(self, request):
185
+ try:
186
+ return super().__call__(request)
187
+ except Exception as exc:
188
+ custom_excepthook(type(exc), exc, exc.__traceback__)
189
+ raise # preserve default Django 500
190
+
191
+ # ------------------------------------------------------------------ #
192
+ # 3 | Header capture
193
+ # ------------------------------------------------------------------ #
194
+ def process_request(self, request):
195
+ # CRITICAL: Clear trace_id FIRST at request start to ensure fresh start
196
+ # Django reuses threads, so we must clear the ContextVar from previous request
197
+ try:
198
+ clear_trace_id()
199
+ clear_outbound_header_base()
200
+ clear_c_tls_parent_trace_id()
201
+ clear_current_request_path()
202
+ if SF_DEBUG and app_config._interceptors_initialized:
203
+ print(
204
+ f"[[SailfishMiddleware.process_request]] Cleared all context at request start",
205
+ log=False,
206
+ )
207
+ except Exception as e:
208
+ if SF_DEBUG and app_config._interceptors_initialized:
209
+ print(
210
+ f"[[SailfishMiddleware.process_request]] Failed to clear context: {e}",
211
+ log=False,
212
+ )
213
+
214
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
215
+ set_current_request_path(request.path)
216
+
217
+ # PERFORMANCE: Single-pass bytes-level header scan (convert Django META to bytes for consistent scanning)
218
+ # Django stores headers in META with HTTP_ prefix, scan once and extract what we need
219
+ incoming_trace_raw = None
220
+ funcspan_override_header = None
221
+
222
+ # Django uses string headers in META, scan for our headers
223
+ header_key = f"HTTP_{SAILFISH_TRACING_HEADER.upper().replace('-', '_')}"
224
+ incoming_trace_raw = request.META.get(header_key)
225
+
226
+ funcspan_override_key = "HTTP_X_SF3_FUNCTIONSPANCAPTUREOVERRIDE"
227
+ funcspan_override_header = request.META.get(funcspan_override_key)
228
+
229
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
230
+ if incoming_trace_raw:
231
+ # Incoming X-Sf3-Rid header provided - use it
232
+ get_or_set_sf_trace_id(
233
+ incoming_trace_raw, is_associated_with_inbound_request=True
234
+ )
235
+ if SF_DEBUG and app_config._interceptors_initialized:
236
+ trace_id = get_sf_trace_id()
237
+ print(
238
+ f"[[SailfishMiddleware.process_request]] "
239
+ f"Using incoming trace: {incoming_trace_raw} → trace_id={trace_id}",
240
+ log=False,
241
+ )
242
+ else:
243
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
244
+ new_trace = generate_new_trace_id()
245
+ if SF_DEBUG and app_config._interceptors_initialized:
246
+ trace_id = get_sf_trace_id()
247
+ print(
248
+ f"[[SailfishMiddleware.process_request]] "
249
+ f"Generated new trace_id: {new_trace} → trace_id={trace_id}",
250
+ log=False,
251
+ )
252
+
253
+ # Optional funcspan override
254
+ if funcspan_override_header:
255
+ try:
256
+ _sffuncspan_config.set_thread_override(funcspan_override_header)
257
+ if SF_DEBUG and app_config._interceptors_initialized:
258
+ print(
259
+ f"[[SailfishMiddleware.process_request]] Set function span override from header: {funcspan_override_header}",
260
+ log=False,
261
+ )
262
+ except Exception as e:
263
+ if SF_DEBUG and app_config._interceptors_initialized:
264
+ print(
265
+ f"[[SailfishMiddleware.process_request]] Failed to set function span override: {e}",
266
+ log=False,
267
+ )
268
+
269
+ # Initialize outbound header base with parent trace ID
270
+ try:
271
+ trace_id = get_sf_trace_id()
272
+ if trace_id:
273
+ s = str(trace_id)
274
+ i = s.find("/") # session
275
+ j = s.find("/", i + 1) if i != -1 else -1 # page
276
+ if j != -1:
277
+ base_trace = s[:j] # "session/page"
278
+ set_outbound_header_base(
279
+ base_trace=base_trace,
280
+ parent_trace_id=s, # "session/page/uuid"
281
+ funcspan=funcspan_override_header,
282
+ )
283
+ if SF_DEBUG and app_config._interceptors_initialized:
284
+ print(
285
+ f"[[SailfishMiddleware.process_request]] Initialized outbound header base (base={base_trace[:16] if len(base_trace) > 16 else base_trace}...)",
286
+ log=False,
287
+ )
288
+ except Exception as e:
289
+ if SF_DEBUG and app_config._interceptors_initialized:
290
+ print(
291
+ f"[[SailfishMiddleware.process_request]] Failed to initialize outbound header base: {e}",
292
+ log=False,
293
+ )
294
+
295
+ # ------------------------------------------------------------------ #
296
+ # 4 | Network-hop emission (unchanged)
297
+ # ------------------------------------------------------------------ #
298
+ def process_view(self, request, view_func, view_args, view_kwargs):
299
+ if SF_DEBUG and app_config._interceptors_initialized:
300
+ print(
301
+ f"[[SailfishMiddleware.process_view]] view_func={view_func.__name__ if hasattr(view_func, '__name__') else view_func}, "
302
+ f"path={request.path}",
303
+ log=False,
304
+ )
305
+
306
+ module = getattr(view_func, "__module__", "")
307
+ if module.startswith("strawberry"):
308
+ if SF_DEBUG and app_config._interceptors_initialized:
309
+ print(
310
+ f"[[Django.process_view]] Skipping Strawberry GraphQL view",
311
+ log=False,
312
+ )
313
+ return None
314
+
315
+ # Unwrap decorated views to get the actual user code
316
+ # Django decorators (csrf_exempt, require_http_methods, etc.) wrap views
317
+ actual_view = _unwrap_user_func(view_func)
318
+
319
+ if actual_view is not view_func and SF_DEBUG and app_config._interceptors_initialized:
320
+ print(
321
+ f"[[Django.process_view]] Unwrapped decorator: "
322
+ f"{view_func.__name__} → {getattr(actual_view, '__name__', 'unknown')}",
323
+ log=False,
324
+ )
325
+
326
+ # Get code object and verify it's user code
327
+ code = getattr(actual_view, "__code__", None)
328
+ if not code:
329
+ if SF_DEBUG and app_config._interceptors_initialized:
330
+ print(
331
+ f"[[Django.process_view]] No code object for view_func", log=False
332
+ )
333
+ return None
334
+
335
+ fname, lno = code.co_filename, code.co_firstlineno
336
+
337
+ # The unwrap function already checks for user code, but double-check
338
+ if not _is_user_code(fname):
339
+ if SF_DEBUG and app_config._interceptors_initialized:
340
+ print(f"[[Django.process_view]] Not user code: {fname}", log=False)
341
+ return None
342
+
343
+ # Extract route pattern from Django's resolver
344
+ route_pattern = None
345
+ if hasattr(request, "resolver_match") and request.resolver_match:
346
+ route_pattern = getattr(request.resolver_match, "route", None)
347
+
348
+ # Check if route should be skipped
349
+ if should_skip_route(route_pattern, _ROUTES_TO_SKIP):
350
+ if SF_DEBUG and app_config._interceptors_initialized:
351
+ print(
352
+ f"[[Django.process_view]] Skipping view (route matches skip pattern): {route_pattern}",
353
+ log=False,
354
+ )
355
+ return None
356
+
357
+ # Get or register endpoint_id (use actual_view for consistent tracking)
358
+ view_id = id(actual_view)
359
+ endpoint_id = _ENDPOINT_REGISTRY.get(view_id)
360
+
361
+ if endpoint_id is None:
362
+ # First time seeing this view - register it
363
+ view_name = getattr(actual_view, "__name__", "unknown")
364
+ endpoint_id = register_endpoint(
365
+ line=str(lno),
366
+ column="0",
367
+ name=view_name,
368
+ entrypoint=fname,
369
+ route=route_pattern,
370
+ )
371
+
372
+ if endpoint_id >= 0:
373
+ _ENDPOINT_REGISTRY[view_id] = endpoint_id
374
+ if SF_DEBUG and app_config._interceptors_initialized:
375
+ print(
376
+ f"[[Django]] Registered endpoint: {view_name} "
377
+ f"({fname}:{lno}) → id={endpoint_id}",
378
+ log=False,
379
+ )
380
+ else:
381
+ # Failed to register, don't track
382
+ return None
383
+
384
+ # Store endpoint_id for process_response()
385
+ request._sf_endpoint_id = endpoint_id
386
+
387
+ # Capture request headers if enabled
388
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
389
+ try:
390
+ # Django stores headers in request.META with HTTP_ prefix
391
+ headers = {}
392
+ for key, value in request.META.items():
393
+ if key.startswith("HTTP_"):
394
+ # Remove HTTP_ prefix and convert to standard format
395
+ header_name = key[5:].replace("_", "-")
396
+ headers[header_name] = str(value)
397
+ elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
398
+ headers[key.replace("_", "-")] = str(value)
399
+ request._sf_request_headers = headers
400
+ except Exception:
401
+ request._sf_request_headers = None
402
+
403
+ # Capture request body if enabled
404
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
405
+ try:
406
+ # Read body (Django caches it, so this is safe)
407
+ limit = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
408
+ body = request.body if hasattr(request, "body") else b""
409
+ if body and len(body) > limit:
410
+ body = body[:limit]
411
+ request._sf_request_body = body if body else None
412
+ except Exception as e:
413
+ if SF_DEBUG and app_config._interceptors_initialized:
414
+ print(f"[[Django]] Failed to capture request body: {e}", log=False)
415
+ request._sf_request_body = None
416
+
417
+ return None
418
+
419
+ # ------------------------------------------------------------------ #
420
+ # 5 | View-level exception hook (unchanged)
421
+ # ------------------------------------------------------------------ #
422
+ def process_response(self, request, response):
423
+ """
424
+ Emit network hop AFTER response is built (OTEL-style zero-overhead).
425
+ Uses pre-registered endpoint_id for ultra-fast C path.
426
+ Captures response headers/body if enabled.
427
+ """
428
+ endpoint_id = getattr(request, "_sf_endpoint_id", None)
429
+
430
+ if SF_DEBUG and app_config._interceptors_initialized:
431
+ print(
432
+ f"[[SailfishMiddleware.process_response]] endpoint_id={endpoint_id}, "
433
+ f"has_endpoint_attr={hasattr(request, '_sf_endpoint_id')}",
434
+ log=False,
435
+ )
436
+
437
+ if endpoint_id is not None and endpoint_id >= 0:
438
+ try:
439
+ _, session_id = get_or_set_sf_trace_id()
440
+
441
+ if SF_DEBUG and app_config._interceptors_initialized:
442
+ print(
443
+ f"[[SailfishMiddleware.process_response]] session_id={session_id}, "
444
+ f"endpoint_id={endpoint_id}, path={request.path}",
445
+ log=False,
446
+ )
447
+
448
+ # Capture response headers if enabled
449
+ resp_headers = None
450
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
451
+ try:
452
+ resp_headers = dict(response.items())
453
+ except Exception as e:
454
+ if SF_DEBUG and app_config._interceptors_initialized:
455
+ print(
456
+ f"[[Django]] Failed to capture response headers: {e}",
457
+ log=False,
458
+ )
459
+
460
+ # Capture response body if enabled
461
+ resp_body = None
462
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
463
+ try:
464
+ limit = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
465
+ if hasattr(response, "content"):
466
+ resp_body = response.content[:limit]
467
+ except Exception as e:
468
+ if SF_DEBUG and app_config._interceptors_initialized:
469
+ print(
470
+ f"[[Django]] Failed to capture response body: {e}",
471
+ log=False,
472
+ )
473
+
474
+ # Get request data if captured
475
+ req_headers = getattr(request, "_sf_request_headers", None)
476
+ req_body = getattr(request, "_sf_request_body", None)
477
+
478
+ # Extract raw path and query string for C to parse
479
+ raw_path = request.path # e.g., "/log"
480
+ raw_query = request.META.get("QUERY_STRING", "").encode(
481
+ "utf-8"
482
+ ) # e.g., b"foo=5"
483
+
484
+ if SF_DEBUG and app_config._interceptors_initialized:
485
+ print(
486
+ f"[[Django]] About to emit network hop: endpoint_id={endpoint_id}, "
487
+ f"req_headers={'present' if req_headers else 'None'}, "
488
+ f"req_body={len(req_body) if req_body else 0} bytes, "
489
+ f"resp_headers={'present' if resp_headers else 'None'}, "
490
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
491
+ log=False,
492
+ )
493
+
494
+ # Direct C call - queues to background worker, returns instantly
495
+ # C will parse route and query_params from raw data
496
+ fast_send_network_hop_fast(
497
+ session_id=session_id,
498
+ endpoint_id=endpoint_id,
499
+ raw_path=raw_path,
500
+ raw_query_string=raw_query,
501
+ request_headers=req_headers,
502
+ request_body=req_body,
503
+ response_headers=resp_headers,
504
+ response_body=resp_body,
505
+ )
506
+
507
+ if SF_DEBUG and app_config._interceptors_initialized:
508
+ print(
509
+ f"[[Django]] Emitted network hop: endpoint_id={endpoint_id} "
510
+ f"session={session_id}",
511
+ log=False,
512
+ )
513
+ except Exception as e: # noqa: BLE001 S110
514
+ if SF_DEBUG and app_config._interceptors_initialized:
515
+ print(f"[[Django]] Failed to emit network hop: {e}", log=False)
516
+
517
+ traceback.print_exc()
518
+
519
+ # Clear function span override for this request (thread-local cleanup)
520
+ try:
521
+ _sffuncspan_config.clear_thread_override()
522
+ except Exception:
523
+ pass
524
+
525
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
526
+ try:
527
+ clear_c_tls_parent_trace_id()
528
+ except Exception:
529
+ pass
530
+
531
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
532
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
533
+ try:
534
+ clear_outbound_header_base()
535
+ except Exception:
536
+ pass
537
+
538
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
539
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
540
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
541
+ try:
542
+ clear_trace_id()
543
+ except Exception:
544
+ pass
545
+
546
+ return response
547
+
548
+ # ------------------------------------------------------------------ #
549
+ # 6 | View-level exception hook (unchanged)
550
+ # ------------------------------------------------------------------ #
551
+ def process_exception(self, request, exception):
552
+ print("[[SailfishMiddleware.process_exception]]", log=False)
553
+ custom_excepthook(type(exception), exception, exception.__traceback__)
554
+
555
+
556
+ # --------------------------------------------------------------------------- #
557
+ # Helper – patch django.core.wsgi.get_wsgi_application once
558
+ # --------------------------------------------------------------------------- #
559
+ # --------------------------------------------------------------------------- #
560
+ # Helper – patch django.core.asgi.get_asgi_application once
561
+ # --------------------------------------------------------------------------- #
562
+ def _patch_get_asgi_application() -> None:
563
+ """
564
+ Replace ``django.core.asgi.get_asgi_application`` with a wrapper that:
565
+
566
+ 1. Runs ``django.setup()`` (as the original does),
567
+ 2. **Then** injects ``SailfishMiddleware`` into *settings.MIDDLEWARE*
568
+ *after* settings are configured but *before* the first ``ASGIHandler``
569
+ is built,
570
+ 3. Returns the handler (ASGI handlers handle exceptions internally).
571
+
572
+ This mirrors the WSGI patching approach.
573
+ """
574
+ try:
575
+ from django.core import asgi as _asgi_mod
576
+ except ImportError: # pragma: no cover
577
+ return
578
+
579
+ if getattr(_asgi_mod, "_sf_patched", False):
580
+ return # idempotent
581
+
582
+ _orig_get_asgi = _asgi_mod.get_asgi_application
583
+ _MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
584
+
585
+ def _sf_get_asgi_application(*args, **kwargs):
586
+ # --- Step 1: exactly replicate original behaviour -----------------
587
+ import django
588
+
589
+ django.setup(set_prefix=False) # configures settings & apps
590
+
591
+ # --- Step 2: inject middleware *now* (settings are configured) ----
592
+ from django.conf import settings
593
+
594
+ if (
595
+ hasattr(settings, "MIDDLEWARE")
596
+ and isinstance(settings.MIDDLEWARE, list)
597
+ and _MW_PATH not in settings.MIDDLEWARE
598
+ ):
599
+ settings.MIDDLEWARE.insert(0, _MW_PATH)
600
+ if SF_DEBUG and app_config._interceptors_initialized:
601
+ print(f"[[_patch_get_asgi_application]] Injected {_MW_PATH}", log=False)
602
+
603
+ # --- Step 2.5: inject CORS headers if configured ----
604
+ if hasattr(settings, 'CORS_ALLOW_HEADERS'):
605
+ original_headers = settings.CORS_ALLOW_HEADERS
606
+
607
+ if SF_DEBUG and app_config._interceptors_initialized:
608
+ print(
609
+ f"[[_patch_get_asgi_application]] Found CORS_ALLOW_HEADERS: {original_headers}",
610
+ log=False,
611
+ )
612
+
613
+ if should_inject_headers(original_headers):
614
+ patched_headers = inject_sailfish_headers(original_headers)
615
+ settings.CORS_ALLOW_HEADERS = patched_headers
616
+
617
+ if SF_DEBUG and app_config._interceptors_initialized:
618
+ print(
619
+ f"[[_patch_get_asgi_application]] Injected Sailfish headers into CORS_ALLOW_HEADERS: {patched_headers}",
620
+ log=False,
621
+ )
622
+
623
+ # --- Step 3: build and return ASGI handler ----
624
+ return _orig_get_asgi(*args, **kwargs)
625
+
626
+ _asgi_mod.get_asgi_application = _sf_get_asgi_application
627
+ _asgi_mod._sf_patched = True
628
+
629
+
630
+ # --------------------------------------------------------------------------- #
631
+ # Helper – patch django.core.wsgi.get_wsgi_application once
632
+ # --------------------------------------------------------------------------- #
633
+ def _patch_get_wsgi_application() -> None:
634
+ """
635
+ Replace ``django.core.wsgi.get_wsgi_application`` with a wrapper that:
636
+
637
+ 1. Runs ``django.setup()`` (as the original does),
638
+ 2. **Then** injects ``SailfishMiddleware`` into *settings.MIDDLEWARE*
639
+ *after* settings are configured but *before* the first ``WSGIHandler``
640
+ is built,
641
+ 3. Wraps the returned handler in our ``CustomExceptionMiddleware`` so we
642
+ still have a last-chance catcher outside Django's stack.
643
+
644
+ This mirrors the flow used by Sentry's Django integration.
645
+ """
646
+ try:
647
+ from django.core import wsgi as _wsgi_mod
648
+ except ImportError: # pragma: no cover
649
+ return
650
+
651
+ if getattr(_wsgi_mod, "_sf_patched", False):
652
+ return # idempotent
653
+
654
+ _orig_get_wsgi = _wsgi_mod.get_wsgi_application
655
+ _MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
656
+
657
+ def _sf_get_wsgi_application(*args, **kwargs):
658
+ # --- Step 1: exactly replicate original behaviour -----------------
659
+ import django
660
+
661
+ django.setup(set_prefix=False) # configures settings & apps
662
+
663
+ # --- Step 2: inject middleware *now* (settings are configured) ----
664
+ from django.conf import settings
665
+
666
+ if (
667
+ hasattr(settings, "MIDDLEWARE")
668
+ and isinstance(settings.MIDDLEWARE, list)
669
+ and _MW_PATH not in settings.MIDDLEWARE
670
+ ):
671
+ settings.MIDDLEWARE.insert(0, _MW_PATH)
672
+
673
+ # --- Step 2.5: inject CORS headers if configured ----
674
+ if hasattr(settings, 'CORS_ALLOW_HEADERS'):
675
+ original_headers = settings.CORS_ALLOW_HEADERS
676
+
677
+ if SF_DEBUG and app_config._interceptors_initialized:
678
+ print(
679
+ f"[[_patch_get_wsgi_application]] Found CORS_ALLOW_HEADERS: {original_headers}",
680
+ log=False,
681
+ )
682
+
683
+ if should_inject_headers(original_headers):
684
+ patched_headers = inject_sailfish_headers(original_headers)
685
+ settings.CORS_ALLOW_HEADERS = patched_headers
686
+
687
+ if SF_DEBUG and app_config._interceptors_initialized:
688
+ print(
689
+ f"[[_patch_get_wsgi_application]] Injected Sailfish headers into CORS_ALLOW_HEADERS: {patched_headers}",
690
+ log=False,
691
+ )
692
+
693
+ # --- Step 3: build handler and wrap for last-chance exceptions ----
694
+ from django.core.handlers.wsgi import WSGIHandler
695
+ from sf_veritas.patches.web_frameworks.django import CustomExceptionMiddleware
696
+
697
+ handler = WSGIHandler()
698
+ return CustomExceptionMiddleware(handler)
699
+
700
+ _wsgi_mod.get_wsgi_application = _sf_get_wsgi_application
701
+ _wsgi_mod._sf_patched = True
702
+
703
+
704
+ def patch_django_middleware(routes_to_skip: Optional[List[str]] = None) -> None:
705
+ """
706
+ Public entry-point called by ``setup_interceptors``.
707
+
708
+ • Inserts ``SailfishMiddleware`` for *already-configured* settings
709
+ (run-server or ASGI).
710
+ • Patches ``get_wsgi_application`` so *future* WSGI handlers created
711
+ by third-party code inherit the middleware without relying on a
712
+ configured settings object at import time.
713
+ """
714
+ global _ROUTES_TO_SKIP
715
+ _ROUTES_TO_SKIP = routes_to_skip or []
716
+
717
+ try:
718
+ from django.conf import settings
719
+ from django.core.exceptions import ImproperlyConfigured
720
+ except ImportError: # Django not installed
721
+ return
722
+
723
+ _MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
724
+
725
+ # ---------- If settings are *already* configured, patch immediately ---
726
+ try:
727
+ if settings.configured and isinstance(
728
+ getattr(settings, "MIDDLEWARE", None), list
729
+ ):
730
+ if _MW_PATH not in settings.MIDDLEWARE:
731
+ settings.MIDDLEWARE.insert(0, _MW_PATH)
732
+ except ImproperlyConfigured:
733
+ # Settings not yet configured – safe to ignore; the WSGI patch below
734
+ # will handle insertion once ``django.setup()`` runs.
735
+ pass
736
+
737
+ # ---------- Always patch get_wsgi/asgi_application (idempotent) ------------
738
+ _patch_get_wsgi_application()
739
+ _patch_get_asgi_application()
740
+
741
+ # ---------- Patch CORS to inject Sailfish headers (idempotent) ------------
742
+ patch_django_cors()
743
+
744
+ if SF_DEBUG and app_config._interceptors_initialized:
745
+ print(
746
+ "[[patch_django_middleware]] Sailfish Django integration ready", log=False
747
+ )
748
+
749
+
750
+ class CustomExceptionMiddleware:
751
+ """
752
+ A universal last-chance exception wrapper that works for either
753
+ • ASGI call signature: (scope, receive, send) → coroutine
754
+ • WSGI call signature: (environ, start_response) → iterable
755
+ Every un-handled exception is funneled through ``custom_excepthook`` once.
756
+ """
757
+
758
+ def __init__(self, app):
759
+ self.app = app
760
+
761
+ # ------------------------------------------------------------------ #
762
+ # Dispatcher – routes ASGI vs WSGI based on arity / argument shape
763
+ # ------------------------------------------------------------------ #
764
+ def __call__(self, *args, **kwargs):
765
+ if len(args) == 3:
766
+ # Heuristic: (scope, receive, send) for ASGI
767
+ return self._asgi_call(*args) # returns coroutine
768
+ # Else assume classic WSGI: (environ, start_response)
769
+ return self._wsgi_call(*args) # returns iterable
770
+
771
+ # ------------------------------------------------------------------ #
772
+ # ASGI branch
773
+ # ------------------------------------------------------------------ #
774
+ async def _asgi_call(self, scope, receive, send):
775
+ try:
776
+ await self.app(scope, receive, send)
777
+ except Exception as exc: # noqa: BLE001
778
+ custom_excepthook(type(exc), exc, exc.__traceback__)
779
+ raise
780
+ finally:
781
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
782
+ try:
783
+ clear_c_tls_parent_trace_id()
784
+ except Exception:
785
+ pass
786
+
787
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
788
+ try:
789
+ clear_outbound_header_base()
790
+ except Exception:
791
+ pass
792
+
793
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
794
+ try:
795
+ clear_trace_id()
796
+ except Exception:
797
+ pass
798
+
799
+ # ------------------------------------------------------------------ #
800
+ # WSGI branch
801
+ # ------------------------------------------------------------------ #
802
+ def _wsgi_call(self, environ, start_response):
803
+ try:
804
+ return self.app(environ, start_response)
805
+ except Exception as exc: # noqa: BLE001
806
+ custom_excepthook(type(exc), exc, exc.__traceback__)
807
+ raise
808
+ finally:
809
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
810
+ try:
811
+ clear_c_tls_parent_trace_id()
812
+ except Exception:
813
+ pass
814
+
815
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
816
+ try:
817
+ clear_outbound_header_base()
818
+ except Exception:
819
+ pass
820
+
821
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
822
+ try:
823
+ clear_trace_id()
824
+ except Exception:
825
+ pass
826
+
827
+ # ------------------------------------------------------------------ #
828
+ # Delegate attribute access so the wrapped app still behaves normally
829
+ # ------------------------------------------------------------------ #
830
+ def __getattr__(self, attr):
831
+ return getattr(self.app, attr)
832
+
833
+
834
+ # --------------------------------------------------------------------------- #
835
+ # CORS Header Injection – django-cors-headers
836
+ # --------------------------------------------------------------------------- #
837
+ def patch_django_cors():
838
+ """
839
+ Patch django-cors-headers to automatically inject Sailfish headers.
840
+
841
+ Two-pronged approach:
842
+ 1. Directly modify Django settings.CORS_ALLOW_HEADERS if already configured
843
+ 2. Patch corsheaders.conf property to inject headers dynamically
844
+
845
+ SAFE: Only modifies CORS if django-cors-headers is installed and configured.
846
+ """
847
+ try:
848
+ from django.conf import settings
849
+ except ImportError:
850
+ if SF_DEBUG and app_config._interceptors_initialized:
851
+ print(
852
+ "[[patch_django_cors]] Django not available, skipping",
853
+ log=False,
854
+ )
855
+ return
856
+
857
+ try:
858
+ from corsheaders import conf as cors_conf
859
+ except ImportError:
860
+ # django-cors-headers not installed, skip patching
861
+ if SF_DEBUG and app_config._interceptors_initialized:
862
+ print(
863
+ "[[patch_django_cors]] django-cors-headers not installed, skipping",
864
+ log=False,
865
+ )
866
+ return
867
+
868
+ # Check if already patched
869
+ if hasattr(cors_conf, "_sf_cors_patched"):
870
+ if SF_DEBUG and app_config._interceptors_initialized:
871
+ print("[[patch_django_cors]] Already patched, skipping", log=False)
872
+ return
873
+
874
+ # APPROACH 1: Directly modify Django settings if CORS is configured
875
+ try:
876
+ if hasattr(settings, 'CORS_ALLOW_HEADERS'):
877
+ original_headers = settings.CORS_ALLOW_HEADERS
878
+
879
+ if SF_DEBUG and app_config._interceptors_initialized:
880
+ print(
881
+ f"[[patch_django_cors]] Found CORS_ALLOW_HEADERS in settings: {original_headers}",
882
+ log=False,
883
+ )
884
+
885
+ if should_inject_headers(original_headers):
886
+ patched_headers = inject_sailfish_headers(original_headers)
887
+ settings.CORS_ALLOW_HEADERS = patched_headers
888
+
889
+ if SF_DEBUG and app_config._interceptors_initialized:
890
+ print(
891
+ f"[[patch_django_cors]] Modified settings.CORS_ALLOW_HEADERS to: {patched_headers}",
892
+ log=False,
893
+ )
894
+ except Exception as e:
895
+ if SF_DEBUG and app_config._interceptors_initialized:
896
+ print(f"[[patch_django_cors]] Failed to modify settings: {e}", log=False)
897
+
898
+ # APPROACH 2: Patch the Conf class property for dynamic access
899
+ try:
900
+ conf_class = type(cors_conf)
901
+
902
+ if hasattr(conf_class, 'CORS_ALLOW_HEADERS'):
903
+ original_property = getattr(conf_class, 'CORS_ALLOW_HEADERS')
904
+
905
+ if isinstance(original_property, property):
906
+ original_fget = original_property.fget
907
+
908
+ def patched_fget(self):
909
+ original_headers = original_fget(self)
910
+
911
+ if should_inject_headers(original_headers):
912
+ patched_headers = inject_sailfish_headers(original_headers)
913
+
914
+ if SF_DEBUG and app_config._interceptors_initialized:
915
+ print(
916
+ f"[[patch_django_cors]] Property access: injected headers -> {patched_headers}",
917
+ log=False,
918
+ )
919
+
920
+ return patched_headers
921
+
922
+ return original_headers
923
+
924
+ setattr(
925
+ conf_class,
926
+ 'CORS_ALLOW_HEADERS',
927
+ property(patched_fget, original_property.fset, original_property.fdel)
928
+ )
929
+
930
+ if SF_DEBUG and app_config._interceptors_initialized:
931
+ print(
932
+ "[[patch_django_cors]] Successfully patched CORS_ALLOW_HEADERS property",
933
+ log=False,
934
+ )
935
+ except Exception as e:
936
+ if SF_DEBUG and app_config._interceptors_initialized:
937
+ print(f"[[patch_django_cors]] Failed to patch property: {e}", log=False)
938
+
939
+ cors_conf._sf_cors_patched = True
940
+
941
+ if SF_DEBUG and app_config._interceptors_initialized:
942
+ print(
943
+ "[[patch_django_cors]] Successfully patched django-cors-headers", log=False
944
+ )