sf-veritas 0.11.10__cp314-cp314-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.
Files changed (141) hide show
  1. sf_veritas/__init__.py +46 -0
  2. sf_veritas/_auto_preload.py +73 -0
  3. sf_veritas/_sfconfig.c +162 -0
  4. sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
  5. sf_veritas/_sfcrashhandler.c +267 -0
  6. sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
  7. sf_veritas/_sffastlog.c +953 -0
  8. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  9. sf_veritas/_sffastnet.c +994 -0
  10. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  11. sf_veritas/_sffastnetworkrequest.c +727 -0
  12. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  13. sf_veritas/_sffuncspan.c +2791 -0
  14. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  15. sf_veritas/_sffuncspan_config.c +730 -0
  16. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  17. sf_veritas/_sfheadercheck.c +341 -0
  18. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  19. sf_veritas/_sfnetworkhop.c +1454 -0
  20. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  21. sf_veritas/_sfservice.c +1223 -0
  22. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  23. sf_veritas/_sfteepreload.c +6227 -0
  24. sf_veritas/app_config.py +57 -0
  25. sf_veritas/cli.py +336 -0
  26. sf_veritas/constants.py +10 -0
  27. sf_veritas/custom_excepthook.py +304 -0
  28. sf_veritas/custom_log_handler.py +146 -0
  29. sf_veritas/custom_output_wrapper.py +153 -0
  30. sf_veritas/custom_print.py +153 -0
  31. sf_veritas/django_app.py +5 -0
  32. sf_veritas/env_vars.py +186 -0
  33. sf_veritas/exception_handling_middleware.py +18 -0
  34. sf_veritas/exception_metaclass.py +69 -0
  35. sf_veritas/fast_frame_info.py +116 -0
  36. sf_veritas/fast_network_hop.py +293 -0
  37. sf_veritas/frame_tools.py +112 -0
  38. sf_veritas/funcspan_config_loader.py +693 -0
  39. sf_veritas/function_span_profiler.py +1313 -0
  40. sf_veritas/get_preload_path.py +34 -0
  41. sf_veritas/import_hook.py +62 -0
  42. sf_veritas/infra_details/__init__.py +3 -0
  43. sf_veritas/infra_details/get_infra_details.py +24 -0
  44. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  45. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  46. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  47. sf_veritas/infra_details/running_on/__init__.py +17 -0
  48. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  49. sf_veritas/interceptors.py +543 -0
  50. sf_veritas/libsfnettee.so +0 -0
  51. sf_veritas/local_env_detect.py +118 -0
  52. sf_veritas/package_metadata.py +6 -0
  53. sf_veritas/patches/__init__.py +0 -0
  54. sf_veritas/patches/_patch_tracker.py +74 -0
  55. sf_veritas/patches/concurrent_futures.py +19 -0
  56. sf_veritas/patches/constants.py +1 -0
  57. sf_veritas/patches/exceptions.py +82 -0
  58. sf_veritas/patches/multiprocessing.py +32 -0
  59. sf_veritas/patches/network_libraries/__init__.py +99 -0
  60. sf_veritas/patches/network_libraries/aiohttp.py +294 -0
  61. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  62. sf_veritas/patches/network_libraries/http_client.py +670 -0
  63. sf_veritas/patches/network_libraries/httpcore.py +580 -0
  64. sf_veritas/patches/network_libraries/httplib2.py +315 -0
  65. sf_veritas/patches/network_libraries/httpx.py +557 -0
  66. sf_veritas/patches/network_libraries/niquests.py +218 -0
  67. sf_veritas/patches/network_libraries/pycurl.py +399 -0
  68. sf_veritas/patches/network_libraries/requests.py +595 -0
  69. sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
  70. sf_veritas/patches/network_libraries/tornado.py +360 -0
  71. sf_veritas/patches/network_libraries/treq.py +270 -0
  72. sf_veritas/patches/network_libraries/urllib_request.py +483 -0
  73. sf_veritas/patches/network_libraries/utils.py +598 -0
  74. sf_veritas/patches/os.py +17 -0
  75. sf_veritas/patches/threading.py +231 -0
  76. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  77. sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
  78. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
  79. sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
  80. sf_veritas/patches/web_frameworks/bottle.py +513 -0
  81. sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
  82. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  83. sf_veritas/patches/web_frameworks/django.py +963 -0
  84. sf_veritas/patches/web_frameworks/eve.py +401 -0
  85. sf_veritas/patches/web_frameworks/falcon.py +931 -0
  86. sf_veritas/patches/web_frameworks/fastapi.py +738 -0
  87. sf_veritas/patches/web_frameworks/flask.py +526 -0
  88. sf_veritas/patches/web_frameworks/klein.py +501 -0
  89. sf_veritas/patches/web_frameworks/litestar.py +616 -0
  90. sf_veritas/patches/web_frameworks/pyramid.py +440 -0
  91. sf_veritas/patches/web_frameworks/quart.py +841 -0
  92. sf_veritas/patches/web_frameworks/robyn.py +708 -0
  93. sf_veritas/patches/web_frameworks/sanic.py +874 -0
  94. sf_veritas/patches/web_frameworks/starlette.py +742 -0
  95. sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
  96. sf_veritas/patches/web_frameworks/tornado.py +485 -0
  97. sf_veritas/patches/web_frameworks/utils.py +170 -0
  98. sf_veritas/print_override.py +13 -0
  99. sf_veritas/regular_data_transmitter.py +444 -0
  100. sf_veritas/request_interceptor.py +401 -0
  101. sf_veritas/request_utils.py +550 -0
  102. sf_veritas/segfault_handler.py +116 -0
  103. sf_veritas/server_status.py +1 -0
  104. sf_veritas/shutdown_flag.py +11 -0
  105. sf_veritas/subprocess_startup.py +3 -0
  106. sf_veritas/test_cli.py +145 -0
  107. sf_veritas/thread_local.py +1319 -0
  108. sf_veritas/timeutil.py +114 -0
  109. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  110. sf_veritas/transmitter.py +132 -0
  111. sf_veritas/types.py +47 -0
  112. sf_veritas/unified_interceptor.py +1678 -0
  113. sf_veritas/utils.py +39 -0
  114. sf_veritas-0.11.10.dist-info/METADATA +97 -0
  115. sf_veritas-0.11.10.dist-info/RECORD +141 -0
  116. sf_veritas-0.11.10.dist-info/WHEEL +5 -0
  117. sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
  118. sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
  119. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  120. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  121. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  122. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  123. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  124. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  125. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  126. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  127. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  128. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  129. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  130. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  131. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  132. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  133. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  134. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  135. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  136. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  137. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  138. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  139. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  140. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  141. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,440 @@
1
+ import inspect
2
+ import os
3
+ import sys
4
+ import sysconfig
5
+ import threading
6
+ from functools import lru_cache
7
+ from typing import Any, Callable, List, Optional, Set, Tuple
8
+
9
+ from ... import _sffuncspan, app_config
10
+ from ...constants import (
11
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
12
+ SAILFISH_TRACING_HEADER,
13
+ SAILFISH_TRACING_HEADER_BYTES,
14
+ )
15
+ from ...custom_excepthook import custom_excepthook
16
+ from ...env_vars import (
17
+ SF_DEBUG,
18
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
19
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
20
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
21
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
22
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
23
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
24
+ )
25
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
26
+ from ...thread_local import (
27
+ clear_c_tls_parent_trace_id,
28
+ clear_current_request_path,
29
+ clear_funcspan_override,
30
+ clear_outbound_header_base,
31
+ clear_trace_id,
32
+ generate_new_trace_id,
33
+ get_or_set_sf_trace_id,
34
+ get_sf_trace_id,
35
+ set_current_request_path,
36
+ set_funcspan_override,
37
+ set_outbound_header_base,
38
+ )
39
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
40
+ from .utils import _is_user_code, should_skip_route, reinitialize_log_print_capture_for_worker # cached helpers
41
+
42
+ # Size limits in bytes
43
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
44
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
45
+
46
+ # Pre-registered endpoint IDs
47
+ _ENDPOINT_REGISTRY: dict[tuple, int] = {}
48
+
49
+ # Routes to skip (set by patch_pyramid)
50
+ _ROUTES_TO_SKIP = []
51
+
52
+
53
+ # ------------------------------------------------------------------ #
54
+ # 1.2 Tween factory: header + one-shot profile tracer + exceptions #
55
+ # ------------------------------------------------------------------ #
56
+ def _sf_tracing_tween_factory(handler, registry):
57
+ """
58
+ OTEL-STYLE Pyramid tween that:
59
+ • Reads SAILFISH_TRACING_HEADER header → ContextVar.
60
+ • Captures request headers and body when enabled.
61
+ • Sets a one-shot profiler to capture endpoint metadata and pre-register endpoint.
62
+ • Captures response headers and body when enabled.
63
+ • Emits NetworkHop AFTER handler completes (OTEL-style zero-overhead).
64
+ • Funnels *all* exceptions (including HTTPException) through
65
+ `custom_excepthook` before letting Pyramid continue normal handling.
66
+ """
67
+
68
+ def _tween(request):
69
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
70
+ set_current_request_path(request.path)
71
+
72
+ # ── 1) Propagate incoming trace header ──────────────────────────
73
+ # PERFORMANCE: Single-pass bytes-level header scan (matching FastAPI pattern)
74
+ hdr = request.headers.get(SAILFISH_TRACING_HEADER)
75
+ funcspan_override_header = request.headers.get(
76
+ "X-Sf3-FunctionSpanCaptureOverride"
77
+ )
78
+
79
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
80
+ if hdr:
81
+ get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
82
+ else:
83
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
84
+ generate_new_trace_id()
85
+
86
+ # Check for function span capture override header (highest priority!)
87
+ if funcspan_override_header:
88
+ try:
89
+ set_funcspan_override(funcspan_override_header)
90
+ if SF_DEBUG and app_config._interceptors_initialized:
91
+ print(
92
+ f"[[Pyramid.tween]] Set function span override from header: {funcspan_override_header}",
93
+ log=False,
94
+ )
95
+ except Exception as e:
96
+ if SF_DEBUG and app_config._interceptors_initialized:
97
+ print(
98
+ f"[[Pyramid.tween]] Failed to set function span override: {e}",
99
+ log=False,
100
+ )
101
+
102
+ # Initialize outbound base without list/allocs from split()
103
+ try:
104
+ trace_id = get_sf_trace_id()
105
+ if trace_id:
106
+ s = str(trace_id)
107
+ i = s.find("/") # session
108
+ j = s.find("/", i + 1) if i != -1 else -1 # page
109
+ if j != -1:
110
+ base_trace = s[:j] # "session/page"
111
+ set_outbound_header_base(
112
+ base_trace=base_trace,
113
+ parent_trace_id=s, # "session/page/uuid"
114
+ funcspan=funcspan_override_header,
115
+ )
116
+ if SF_DEBUG and app_config._interceptors_initialized:
117
+ print(
118
+ f"[[Pyramid.tween]] Initialized outbound header base (base={base_trace[:16]}...)",
119
+ log=False,
120
+ )
121
+ except Exception as e:
122
+ if SF_DEBUG and app_config._interceptors_initialized:
123
+ print(
124
+ f"[[Pyramid.tween]] Failed to initialize outbound header base: {e}",
125
+ log=False,
126
+ )
127
+
128
+ # ── 2) Capture request headers if enabled ────────────────────────
129
+ req_headers = None
130
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
131
+ try:
132
+ req_headers = dict(request.headers)
133
+ if SF_DEBUG and app_config._interceptors_initialized:
134
+ print(
135
+ f"[[Pyramid]] Captured request headers: {len(req_headers)} headers",
136
+ log=False,
137
+ )
138
+ except Exception as e:
139
+ if SF_DEBUG and app_config._interceptors_initialized:
140
+ print(
141
+ f"[[Pyramid]] Failed to capture request headers: {e}", log=False
142
+ )
143
+
144
+ # ── 3) Capture request body if enabled ────────────────────────────
145
+ req_body = None
146
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
147
+ try:
148
+ # Pyramid: request.body gives bytes
149
+ body = request.body
150
+ if body:
151
+ req_body = body[:_REQUEST_LIMIT_BYTES]
152
+ if SF_DEBUG and app_config._interceptors_initialized:
153
+ print(
154
+ f"[[Pyramid]] Request body capture: {len(req_body)} bytes",
155
+ log=False,
156
+ )
157
+ except Exception as e:
158
+ if SF_DEBUG and app_config._interceptors_initialized:
159
+ print(f"[[Pyramid]] Failed to capture request body: {e}", log=False)
160
+
161
+ # ── 4) OTEL-STYLE: One-shot tracer to capture endpoint metadata and pre-register ──
162
+ endpoint_id = None
163
+
164
+ def tracer(frame, event, _arg):
165
+ nonlocal endpoint_id
166
+ if event != "call": # only Python calls
167
+ return tracer
168
+ fn_path = frame.f_code.co_filename
169
+ if _is_user_code(fn_path):
170
+ # Skip Strawberry GraphQL handlers
171
+ try:
172
+ fn_module = frame.f_globals.get("__name__", "")
173
+ if fn_module.startswith("strawberry"):
174
+ if SF_DEBUG and app_config._interceptors_initialized:
175
+ print(
176
+ f"[[Pyramid]] Skipping endpoint (Strawberry GraphQL handler): {fn_module}",
177
+ log=False,
178
+ )
179
+ sys.setprofile(None)
180
+ return None
181
+ except Exception:
182
+ pass
183
+
184
+ hop_key = (fn_path, frame.f_lineno)
185
+
186
+ # Get route pattern if available
187
+ route_pattern = request.path
188
+
189
+ # Check if route should be skipped
190
+ if route_pattern and should_skip_route(route_pattern, _ROUTES_TO_SKIP):
191
+ if SF_DEBUG and app_config._interceptors_initialized:
192
+ print(
193
+ f"[[Pyramid]] Skipping endpoint (route matches skip pattern): {route_pattern}",
194
+ log=False,
195
+ )
196
+ sys.setprofile(None)
197
+ return None
198
+
199
+ # Pre-register endpoint if not already registered
200
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
201
+ if endpoint_id is None:
202
+ endpoint_id = register_endpoint(
203
+ line=str(frame.f_lineno),
204
+ column="0",
205
+ name=frame.f_code.co_name,
206
+ entrypoint=fn_path,
207
+ route=route_pattern,
208
+ )
209
+ if endpoint_id >= 0:
210
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
211
+ if SF_DEBUG and app_config._interceptors_initialized:
212
+ print(
213
+ f"[[Pyramid]] Registered endpoint: {frame.f_code.co_name} @ "
214
+ f"{fn_path}:{frame.f_lineno} (id={endpoint_id})",
215
+ log=False,
216
+ )
217
+
218
+ if SF_DEBUG and app_config._interceptors_initialized:
219
+ print(
220
+ f"[[Pyramid]] Captured endpoint: {frame.f_code.co_name} "
221
+ f"({fn_path}:{frame.f_lineno}) endpoint_id={endpoint_id}",
222
+ log=False,
223
+ )
224
+
225
+ sys.setprofile(None) # disable after first capture
226
+ return None
227
+ return tracer
228
+
229
+ sys.setprofile(tracer)
230
+
231
+ # ── 5) Call downstream handler & capture **all** exceptions ─────
232
+ try:
233
+ response = handler(request)
234
+ except Exception as exc: # HTTPException included
235
+ custom_excepthook(type(exc), exc, exc.__traceback__)
236
+ raise # re-raise for Pyramid
237
+ finally:
238
+ sys.setprofile(None) # safety-net cleanup
239
+
240
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
241
+ clear_c_tls_parent_trace_id()
242
+
243
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
244
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
245
+ clear_outbound_header_base()
246
+
247
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
248
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
249
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
250
+ clear_trace_id()
251
+
252
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
253
+ clear_current_request_path()
254
+
255
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
256
+ try:
257
+ clear_funcspan_override()
258
+ except Exception:
259
+ pass
260
+
261
+ # ── 6) Capture response headers if enabled ────────────────────────
262
+ resp_headers = None
263
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
264
+ try:
265
+ resp_headers = dict(response.headers)
266
+ if SF_DEBUG and app_config._interceptors_initialized:
267
+ print(
268
+ f"[[Pyramid]] Captured response headers: {len(resp_headers)} headers",
269
+ log=False,
270
+ )
271
+ except Exception as e:
272
+ if SF_DEBUG and app_config._interceptors_initialized:
273
+ print(
274
+ f"[[Pyramid]] Failed to capture response headers: {e}",
275
+ log=False,
276
+ )
277
+
278
+ # ── 7) Capture response body if enabled ────────────────────────────
279
+ resp_body = None
280
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
281
+ try:
282
+ # Pyramid: response.body gives bytes (or use app_iter for streaming)
283
+ if hasattr(response, "body"):
284
+ body = response.body
285
+ if body:
286
+ resp_body = body[:_RESPONSE_LIMIT_BYTES]
287
+ if SF_DEBUG and app_config._interceptors_initialized:
288
+ print(
289
+ f"[[Pyramid]] Captured response body: {len(resp_body)} bytes",
290
+ log=False,
291
+ )
292
+ except Exception as e:
293
+ if SF_DEBUG and app_config._interceptors_initialized:
294
+ print(
295
+ f"[[Pyramid]] Failed to capture response body: {e}", log=False
296
+ )
297
+
298
+ # ── 8) OTEL-STYLE: Emit network hop AFTER handler completes ─────
299
+ if endpoint_id is not None and endpoint_id >= 0:
300
+ try:
301
+ _, session_id = get_or_set_sf_trace_id()
302
+
303
+ # Extract raw path and query string for C to parse
304
+ raw_path = request.path # e.g., "/log"
305
+ raw_query = (
306
+ request.query_string.encode("utf-8")
307
+ if request.query_string
308
+ else b""
309
+ ) # e.g., b"foo=5"
310
+
311
+ if SF_DEBUG and app_config._interceptors_initialized:
312
+ print(
313
+ f"[[Pyramid]] About to emit network hop: endpoint_id={endpoint_id}, "
314
+ f"req_headers={'present' if req_headers else 'None'}, "
315
+ f"req_body={len(req_body) if req_body else 0} bytes, "
316
+ f"resp_headers={'present' if resp_headers else 'None'}, "
317
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
318
+ log=False,
319
+ )
320
+
321
+ # Direct C call - queues to background worker, returns instantly
322
+ # C will parse route and query_params from raw data
323
+ fast_send_network_hop_fast(
324
+ session_id=session_id,
325
+ endpoint_id=endpoint_id,
326
+ raw_path=raw_path,
327
+ raw_query_string=raw_query,
328
+ request_headers=req_headers,
329
+ request_body=req_body,
330
+ response_headers=resp_headers,
331
+ response_body=resp_body,
332
+ )
333
+
334
+ if SF_DEBUG and app_config._interceptors_initialized:
335
+ print(
336
+ f"[[Pyramid]] Emitted network hop: endpoint_id={endpoint_id} "
337
+ f"session={session_id}",
338
+ log=False,
339
+ )
340
+ except Exception as e: # noqa: BLE001 S110
341
+ if SF_DEBUG and app_config._interceptors_initialized:
342
+ print(f"[[Pyramid]] Failed to emit network hop: {e}", log=False)
343
+
344
+ return response
345
+
346
+ return _tween
347
+
348
+
349
+ # ------------------------------------------------------------------ #
350
+ # 1.3 Monkey-patch Configurator to auto-add our tween #
351
+ # ------------------------------------------------------------------ #
352
+ def patch_pyramid(routes_to_skip: Optional[List[str]] = None):
353
+ """
354
+ Ensure every Pyramid Configurator implicitly registers our tween
355
+ at the INVOCATION stage (just above MAIN).
356
+ """
357
+ global _ROUTES_TO_SKIP
358
+ _ROUTES_TO_SKIP = routes_to_skip or []
359
+
360
+ try:
361
+ import pyramid.config
362
+ import pyramid.tweens
363
+ except ImportError:
364
+ return # Pyramid not installed
365
+
366
+ original_init = pyramid.config.Configurator.__init__
367
+
368
+ def patched_init(self, *args, **kwargs):
369
+ original_init(self, *args, **kwargs)
370
+
371
+ # Note: Profiler is already installed by unified_interceptor.py
372
+
373
+ # Use a dotted name—implicit ordering places it just above MAIN
374
+ dotted = f"{_sf_tracing_tween_factory.__module__}._sf_tracing_tween_factory"
375
+ # 'over=pyramid.tweens.MAIN' ensures our tween runs *before* the main handler
376
+ self.add_tween(dotted, over=pyramid.tweens.MAIN)
377
+
378
+ pyramid.config.Configurator.__init__ = patched_init
379
+
380
+ # ---- CORS patching --------------------------------------------------------
381
+ patch_pyramid_cors()
382
+
383
+
384
+ def patch_pyramid_cors():
385
+ """
386
+ Patch Pyramid's Response to automatically inject Sailfish headers into CORS.
387
+
388
+ SAFE: Only modifies Access-Control-Allow-Headers if the application sets it.
389
+ Pyramid doesn't have a standard CORS library built-in, but users often use
390
+ pyramid-cors or set headers manually. We patch Response.headerlist to intercept.
391
+ """
392
+ try:
393
+ from pyramid.response import Response
394
+ from webob.headers import ResponseHeaders
395
+ except ImportError:
396
+ if SF_DEBUG and app_config._interceptors_initialized:
397
+ print(
398
+ "[[patch_pyramid_cors]] Pyramid Response not available, skipping",
399
+ log=False,
400
+ )
401
+ return
402
+
403
+ # Check if already patched
404
+ if hasattr(ResponseHeaders, "_sf_cors_patched"):
405
+ if SF_DEBUG and app_config._interceptors_initialized:
406
+ print("[[patch_pyramid_cors]] Already patched, skipping", log=False)
407
+ return
408
+
409
+ # Patch ResponseHeaders.__setitem__ to intercept header setting
410
+ # Pyramid uses WebOb's ResponseHeaders for response.headers
411
+ original_setitem = ResponseHeaders.__setitem__
412
+
413
+ def patched_setitem(self, name, value):
414
+ # Intercept Access-Control-Allow-Headers header
415
+ if name.lower() == "access-control-allow-headers":
416
+ if should_inject_headers(value):
417
+ injected = inject_sailfish_headers(value)
418
+ # CRITICAL: Convert list back to comma-separated string for WSGI
419
+ # WSGI requires header values to be strings, not lists
420
+ if isinstance(injected, list):
421
+ value = ", ".join(injected)
422
+ else:
423
+ value = injected
424
+ if SF_DEBUG and app_config._interceptors_initialized:
425
+ print(
426
+ f"[[patch_pyramid_cors]] Injected Sailfish headers into Access-Control-Allow-Headers: {value}",
427
+ log=False,
428
+ )
429
+
430
+ # Call original __setitem__
431
+ return original_setitem(self, name, value)
432
+
433
+ ResponseHeaders.__setitem__ = patched_setitem
434
+ ResponseHeaders._sf_cors_patched = True
435
+
436
+ if SF_DEBUG and app_config._interceptors_initialized:
437
+ print(
438
+ "[[patch_pyramid_cors]] Successfully patched Pyramid ResponseHeaders.__setitem__",
439
+ log=False,
440
+ )