sf-veritas 0.10.3__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.

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