sf-veritas 0.10.3__cp313-cp313-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-313-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-313-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-313-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-313-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-313-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-313-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-313-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-313-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,813 @@
1
+ import inspect
2
+ import logging
3
+ import sys
4
+ import traceback
5
+ from importlib.util import find_spec
6
+ from typing import Any, Callable, Set, Tuple
7
+
8
+ from ... import app_config
9
+ from ...custom_excepthook import custom_excepthook
10
+ from ...env_vars import (
11
+ CAPTURE_STRAWBERRY_ERRORS_WITH_DATA,
12
+ PRINT_CONFIGURATION_STATUSES,
13
+ SF_DEBUG,
14
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
15
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
16
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
17
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
18
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
19
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
20
+ STRAWBERRY_DEBUG,
21
+ )
22
+ from ...fast_network_hop import (
23
+ fast_send_network_hop,
24
+ fast_send_network_hop_fast,
25
+ register_endpoint,
26
+ )
27
+ from ...thread_local import get_or_set_sf_trace_id
28
+ from ...transmit_exception_to_sailfish import transmit_exception_to_sailfish
29
+ from .utils import _is_user_code, _unwrap_user_func
30
+
31
+ # JSON serialization - try fast orjson first, fallback to stdlib json
32
+ try:
33
+ import orjson
34
+
35
+ HAS_ORJSON = True
36
+ except ImportError:
37
+ import json
38
+
39
+
40
+ logger = logging.getLogger(__name__)
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
+ # Track if Strawberry has already been patched to prevent multiple patches
50
+ _is_strawberry_patched = False
51
+
52
+
53
+ # Cache for function definition line numbers (keyed by code object id)
54
+ _FUNCTION_DEF_LINE_CACHE: dict[int, int] = {}
55
+
56
+
57
+ def _get_function_def_line(frame):
58
+ """
59
+ Get the line number of the 'def' statement, skipping decorators.
60
+
61
+ Python's co_firstlineno includes decorators, so we need to scan the source
62
+ to find the actual function definition line.
63
+
64
+ PERFORMANCE: Results are cached by code object ID, so the file I/O only
65
+ happens once per function (first request). Subsequent requests are instant.
66
+ """
67
+ code_id = id(frame.f_code)
68
+
69
+ # Check cache first - this is the fast path for all requests after the first
70
+ if code_id in _FUNCTION_DEF_LINE_CACHE:
71
+ return _FUNCTION_DEF_LINE_CACHE[code_id]
72
+
73
+ # Cache miss - do the expensive source file lookup
74
+ try:
75
+ # Get source lines for this code object (SLOW: reads file from disk)
76
+ source_lines, start_line = inspect.getsourcelines(frame.f_code)
77
+
78
+ # Find the first line that starts with 'def' or 'async def'
79
+ for i, line in enumerate(source_lines):
80
+ stripped = line.strip()
81
+ if stripped.startswith("def ") or stripped.startswith("async def "):
82
+ def_line = start_line + i
83
+ # Cache the result for next time
84
+ _FUNCTION_DEF_LINE_CACHE[code_id] = def_line
85
+ return def_line
86
+
87
+ # Fallback: return co_firstlineno if we can't find def
88
+ result = frame.f_code.co_firstlineno
89
+ _FUNCTION_DEF_LINE_CACHE[code_id] = result
90
+ return result
91
+ except Exception:
92
+ # If anything fails, fallback to co_firstlineno
93
+ result = frame.f_code.co_firstlineno
94
+ _FUNCTION_DEF_LINE_CACHE[code_id] = result
95
+ return result
96
+
97
+
98
+ def get_extension():
99
+ from strawberry.extensions import SchemaExtension
100
+
101
+ class CustomErrorHandlingExtension(SchemaExtension):
102
+ def __init__(self, *, execution_context):
103
+ self.execution_context = execution_context
104
+
105
+ def on_request_start(self):
106
+ if SF_DEBUG and app_config._interceptors_initialized:
107
+ print("Starting GraphQL request", log=False)
108
+
109
+ def on_request_end(self):
110
+ if SF_DEBUG and app_config._interceptors_initialized:
111
+ print("Ending GraphQL request", log=False)
112
+ if not self.execution_context.errors:
113
+ return
114
+ for error in self.execution_context.errors:
115
+ if SF_DEBUG and app_config._interceptors_initialized:
116
+ print(f"Handling GraphQL error: {error}", log=False)
117
+ custom_excepthook(type(error), error, error.__traceback__)
118
+
119
+ def on_validation_start(self):
120
+ if SF_DEBUG and app_config._interceptors_initialized:
121
+ print("Starting validation of GraphQL request", log=False)
122
+
123
+ def on_validation_end(self):
124
+ if SF_DEBUG and app_config._interceptors_initialized:
125
+ print("Ending validation of GraphQL request", log=False)
126
+
127
+ def on_execution_start(self):
128
+ if SF_DEBUG and app_config._interceptors_initialized:
129
+ print("Starting execution of GraphQL request", log=False)
130
+
131
+ def on_resolver_start(self, resolver, obj, info, **kwargs):
132
+ if SF_DEBUG and app_config._interceptors_initialized:
133
+ print(f"Starting resolver {resolver.__name__}", log=False)
134
+
135
+ def on_resolver_end(self, resolver, obj, info, **kwargs):
136
+ if SF_DEBUG and app_config._interceptors_initialized:
137
+ print(f"Ending resolver {resolver.__name__}", log=False)
138
+
139
+ def on_error(self, error: Exception):
140
+ if SF_DEBUG and app_config._interceptors_initialized:
141
+ print(f"Handling error in resolver: {error}", log=False)
142
+ custom_excepthook(type(error), error, error.__traceback__)
143
+
144
+ return CustomErrorHandlingExtension
145
+
146
+
147
+ def get_network_hop_extension() -> "type[SchemaExtension]":
148
+ """
149
+ Strawberry SchemaExtension that emits a collectNetworkHops mutation for the
150
+ *first* user-land frame executed inside every resolver (sync or async).
151
+ """
152
+
153
+ from strawberry.extensions import SchemaExtension
154
+
155
+ # --------------------------------------------------------------------- #
156
+ # Helper predicates
157
+ # --------------------------------------------------------------------- #
158
+ # Extended dig: __wrapped__, closure cells *and* common attribute names
159
+ # --------------------------------------------------------------------- #
160
+ # Extension class
161
+ # --------------------------------------------------------------------- #
162
+ class NetworkHopExtension(SchemaExtension):
163
+ supports_sync = supports_async = True
164
+ _sent: Set[Tuple[str, int]] = set() # class-level: de-dupe per request
165
+
166
+ def __init__(self, *, execution_context):
167
+ super().__init__(execution_context=execution_context)
168
+ self._captured_endpoints = (
169
+ []
170
+ ) # Store endpoint info for post-response emission
171
+ self._request_data = {} # Store request headers/body
172
+ self._response_data = {} # Store response headers/body
173
+
174
+ # ---------------- internal capture helper ---------------- #
175
+ def _capture(self, frame, info):
176
+ """OTEL-STYLE: Capture endpoint metadata and pre-register."""
177
+ filename = frame.f_code.co_filename
178
+ func_name = frame.f_code.co_name
179
+
180
+ # Get the actual function definition line (skipping decorators)
181
+ line_no = _get_function_def_line(frame)
182
+
183
+ if SF_DEBUG and app_config._interceptors_initialized:
184
+ print(
185
+ f"[[Strawberry]] _capture: {func_name} @ {filename} "
186
+ f"co_firstlineno={frame.f_code.co_firstlineno} -> def_line={line_no}",
187
+ log=False,
188
+ )
189
+ if (filename, line_no) in NetworkHopExtension._sent:
190
+ return
191
+
192
+ hop_key = (filename, line_no)
193
+
194
+ # Pre-register endpoint if not already registered
195
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
196
+ if endpoint_id is None:
197
+ endpoint_id = register_endpoint(
198
+ line=str(line_no),
199
+ column="0",
200
+ name=func_name,
201
+ entrypoint=filename,
202
+ route=None,
203
+ )
204
+ if endpoint_id >= 0:
205
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
206
+ if SF_DEBUG and app_config._interceptors_initialized:
207
+ print(
208
+ f"[[Strawberry]] Registered resolver: {func_name} @ "
209
+ f"{filename}:{line_no} (id={endpoint_id})",
210
+ log=False,
211
+ )
212
+
213
+ # Store for on_request_end to emit
214
+ self._captured_endpoints.append(
215
+ {
216
+ "filename": filename,
217
+ "line": line_no,
218
+ "name": func_name,
219
+ "endpoint_id": endpoint_id,
220
+ }
221
+ )
222
+ NetworkHopExtension._sent.add((filename, line_no))
223
+
224
+ if SF_DEBUG and app_config._interceptors_initialized:
225
+ print(
226
+ f"[[Strawberry]] Captured resolver: {func_name} "
227
+ f"({filename}:{line_no}) endpoint_id={endpoint_id}",
228
+ log=False,
229
+ )
230
+
231
+ # ---------------- tracer factory ---------------- #
232
+ def _make_tracer(self, info):
233
+ def tracer(frame, event, arg):
234
+ if event.startswith("c_"):
235
+ return
236
+ if event == "call":
237
+ if _is_user_code(frame.f_code.co_filename):
238
+ self._capture(frame, info)
239
+ sys.setprofile(None)
240
+ return
241
+ return tracer # keep tracing until we hit user code
242
+
243
+ return tracer
244
+
245
+ # ---------------- request/response capture ---------------- #
246
+ def on_request_start(self):
247
+ """Capture GraphQL request data when request starts."""
248
+ # IMPORTANT: Clear captured endpoints from previous requests
249
+ # SchemaExtension instances may be reused across requests
250
+ self._captured_endpoints = []
251
+ self._request_data = {}
252
+ self._response_data = {}
253
+
254
+ if (
255
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS
256
+ or SF_NETWORKHOP_CAPTURE_REQUEST_BODY
257
+ ):
258
+ try:
259
+ # Access the GraphQL query from execution context
260
+ if (
261
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY
262
+ and self.execution_context.query
263
+ ):
264
+
265
+ query_data = {
266
+ "query": self.execution_context.query,
267
+ "variables": self.execution_context.variables or {},
268
+ "operation_name": self.execution_context.operation_name,
269
+ }
270
+ # Convert to JSON string and limit size
271
+ if HAS_ORJSON:
272
+ query_str = orjson.dumps(query_data)[:_REQUEST_LIMIT_BYTES]
273
+ else:
274
+ query_str = json.dumps(query_data)[:_REQUEST_LIMIT_BYTES]
275
+ self._request_data["body"] = query_str.encode("utf-8")
276
+ if SF_DEBUG and app_config._interceptors_initialized:
277
+ print(
278
+ f"[[Strawberry]] Captured GraphQL query: {len(query_str)} chars",
279
+ log=False,
280
+ )
281
+
282
+ # Try to capture HTTP headers if available (depends on integration)
283
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
284
+ # For Django/Flask integrations, headers might be in context
285
+ if hasattr(self.execution_context, "context"):
286
+ ctx = self.execution_context.context
287
+ if hasattr(ctx, "request") and hasattr(
288
+ ctx.request, "headers"
289
+ ):
290
+ self._request_data["headers"] = dict(
291
+ ctx.request.headers
292
+ )
293
+ if SF_DEBUG and app_config._interceptors_initialized:
294
+ print(
295
+ f"[[Strawberry]] Captured {len(self._request_data['headers'])} request headers",
296
+ log=False,
297
+ )
298
+ except Exception as e:
299
+ if SF_DEBUG and app_config._interceptors_initialized:
300
+ print(
301
+ f"[[Strawberry]] Failed to capture request data: {e}",
302
+ log=False,
303
+ )
304
+
305
+ # ---------------- wrappers ---------------- #
306
+ def resolve(self, _next, root, info, *args, **kwargs):
307
+ user_fn = _unwrap_user_func(_next)
308
+ tracer = self._make_tracer(info)
309
+ sys.setprofile(tracer)
310
+ try:
311
+ return _next(root, info, *args, **kwargs)
312
+ finally:
313
+ sys.setprofile(None) # safety-net
314
+
315
+ async def resolve_async(self, _next, root, info, *args, **kwargs):
316
+ user_fn = _unwrap_user_func(_next)
317
+ tracer = self._make_tracer(info)
318
+ sys.setprofile(tracer)
319
+ try:
320
+ return await _next(root, info, *args, **kwargs)
321
+ finally:
322
+ sys.setprofile(None)
323
+
324
+ # ---------------- OTEL-STYLE: Emit after request completes ---------------- #
325
+ def on_request_end(self):
326
+ """Capture response data and emit network hops AFTER GraphQL response is built."""
327
+ # Capture response data first
328
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY and self.execution_context.result:
329
+ try:
330
+ # GraphQL result includes data and errors
331
+ result_data = {
332
+ "data": (
333
+ self.execution_context.result.data
334
+ if self.execution_context.result.data
335
+ else None
336
+ ),
337
+ "errors": (
338
+ [str(e) for e in self.execution_context.result.errors]
339
+ if self.execution_context.result.errors
340
+ else None
341
+ ),
342
+ }
343
+ if HAS_ORJSON:
344
+ result_str = orjson.dumps(result_data, default=str)[
345
+ :_RESPONSE_LIMIT_BYTES
346
+ ]
347
+ else:
348
+ result_str = json.dumps(result_data, default=str)[
349
+ :_RESPONSE_LIMIT_BYTES
350
+ ]
351
+ self._response_data["body"] = result_str.encode("utf-8")
352
+ if SF_DEBUG and app_config._interceptors_initialized:
353
+ print(
354
+ f"[[Strawberry]] Captured GraphQL result: {len(result_str)} chars",
355
+ log=False,
356
+ )
357
+ except Exception as e:
358
+ if SF_DEBUG and app_config._interceptors_initialized:
359
+ print(
360
+ f"[[Strawberry]] Failed to capture response data: {e}",
361
+ log=False,
362
+ )
363
+
364
+ # Get captured data
365
+ req_headers = self._request_data.get("headers")
366
+ req_body = self._request_data.get("body")
367
+ resp_headers = self._response_data.get(
368
+ "headers"
369
+ ) # Not typically available in GraphQL
370
+ resp_body = self._response_data.get("body")
371
+
372
+ # Emit network hops for all captured resolvers
373
+ for endpoint_info in self._captured_endpoints:
374
+ endpoint_id = endpoint_info.get("endpoint_id")
375
+
376
+ try:
377
+ _, session_id = get_or_set_sf_trace_id()
378
+
379
+ if SF_DEBUG and app_config._interceptors_initialized:
380
+ print(
381
+ f"[[Strawberry]] Emitting hop for {endpoint_info['name']}: "
382
+ f"req_headers={'present' if req_headers else 'None'}, "
383
+ f"req_body={len(req_body) if req_body else 0} bytes, "
384
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
385
+ log=False,
386
+ )
387
+
388
+ # Extract raw path and query string for C to parse (if available from context)
389
+ raw_path = None
390
+ raw_query = b""
391
+ try:
392
+ if hasattr(self.execution_context, "context"):
393
+ ctx = self.execution_context.context
394
+ if hasattr(ctx, "request"):
395
+ req = ctx.request
396
+ # Try to get path - different frameworks have different attributes
397
+ if hasattr(req, "path"):
398
+ raw_path = str(req.path)
399
+ elif hasattr(req, "url") and hasattr(req.url, "path"):
400
+ raw_path = str(req.url.path)
401
+
402
+ # Try to get query string
403
+ if hasattr(req, "query_string"):
404
+ raw_query = (
405
+ req.query_string
406
+ if isinstance(req.query_string, bytes)
407
+ else req.query_string.encode("utf-8")
408
+ )
409
+ elif (
410
+ hasattr(req, "META") and "QUERY_STRING" in req.META
411
+ ):
412
+ raw_query = req.META["QUERY_STRING"].encode("utf-8")
413
+ except Exception as e:
414
+ if SF_DEBUG and app_config._interceptors_initialized:
415
+ print(
416
+ f"[[Strawberry]] Failed to extract path/query: {e}",
417
+ log=False,
418
+ )
419
+
420
+ # Use fast path if C extension available
421
+ if endpoint_id is not None and endpoint_id >= 0:
422
+ fast_send_network_hop_fast(
423
+ session_id=session_id,
424
+ endpoint_id=endpoint_id,
425
+ raw_path=raw_path,
426
+ raw_query_string=raw_query,
427
+ request_headers=req_headers,
428
+ request_body=req_body,
429
+ response_headers=resp_headers,
430
+ response_body=resp_body,
431
+ )
432
+ if SF_DEBUG and app_config._interceptors_initialized:
433
+ print(
434
+ f"[[Strawberry]] Emitted network hop (fast path): {endpoint_info['name']} "
435
+ f"endpoint_id={endpoint_id} session={session_id}",
436
+ log=False,
437
+ )
438
+ else:
439
+ # Fallback to old Python API (doesn't support body/header capture)
440
+ fast_send_network_hop(
441
+ session_id=session_id,
442
+ line=str(endpoint_info["line"]),
443
+ column="0",
444
+ name=endpoint_info["name"],
445
+ entrypoint=endpoint_info["filename"],
446
+ )
447
+ if SF_DEBUG and app_config._interceptors_initialized:
448
+ print(
449
+ f"[[Strawberry]] Emitted network hop (fallback): {endpoint_info['name']} "
450
+ f"session={session_id}",
451
+ log=False,
452
+ )
453
+ except Exception as e: # noqa: BLE001 S110
454
+ if SF_DEBUG and app_config._interceptors_initialized:
455
+ print(
456
+ f"[[Strawberry]] Failed to emit network hop: {e}", log=False
457
+ )
458
+
459
+ return NetworkHopExtension
460
+
461
+
462
+ def patch_strawberry_module(strawberry):
463
+ """Patch Strawberry to ensure exceptions go through the custom excepthook."""
464
+ global _is_strawberry_patched
465
+ if _is_strawberry_patched:
466
+ if SF_DEBUG and app_config._interceptors_initialized:
467
+ print(
468
+ "[[DEBUG]] Strawberry has already been patched, skipping. [[/DEBUG]]",
469
+ log=False,
470
+ )
471
+ return
472
+
473
+ try:
474
+ # Backup the original execute method from Strawberry
475
+ original_execute = strawberry.execution.execute.execute
476
+
477
+ async def custom_execute(*args, **kwargs):
478
+ try:
479
+ if SF_DEBUG and app_config._interceptors_initialized:
480
+ print(
481
+ "[[DEBUG]] Executing patched Strawberry execute function. [[/DEBUG]]",
482
+ log=False,
483
+ )
484
+ return await original_execute(*args, **kwargs)
485
+ except Exception as e:
486
+ if SF_DEBUG and app_config._interceptors_initialized:
487
+ print(
488
+ "[[DEBUG]] Intercepted exception in Strawberry execute. [[/DEBUG]]",
489
+ log=False,
490
+ )
491
+ # Invoke custom excepthook globally
492
+ sys.excepthook(type(e), e, e.__traceback__)
493
+ raise
494
+
495
+ # Replace Strawberry's execute function with the patched version
496
+ strawberry.execution.execute.execute = custom_execute
497
+ _is_strawberry_patched = True
498
+ if SF_DEBUG and app_config._interceptors_initialized:
499
+ print(
500
+ "[[DEBUG]] Successfully patched Strawberry execute function. [[/DEBUG]]",
501
+ log=False,
502
+ )
503
+ except Exception as error:
504
+ if SF_DEBUG and app_config._interceptors_initialized:
505
+ print(
506
+ f"[[DEBUG]] Failed to patch Strawberry: {error}. [[/DEBUG]]", log=False
507
+ )
508
+
509
+
510
+ class CustomImportHook:
511
+ """Import hook to intercept the import of 'strawberry' modules."""
512
+
513
+ def find_spec(self, fullname, path, target=None):
514
+ global _is_strawberry_patched
515
+ if fullname == "strawberry" and not _is_strawberry_patched:
516
+ if SF_DEBUG and app_config._interceptors_initialized:
517
+ print(
518
+ f"[[DEBUG]] Intercepting import of {fullname}. [[/DEBUG]]",
519
+ log=False,
520
+ )
521
+ return find_spec(fullname)
522
+ if fullname.startswith("strawberry_django"):
523
+ return None # Let default import handle strawberry_django
524
+
525
+ def exec_module(self, module):
526
+ if SF_DEBUG and app_config._interceptors_initialized:
527
+ print(
528
+ f"[[DEBUG]] Executing module: {module.__name__}. [[/DEBUG]]", log=False
529
+ )
530
+ # Execute the module normally
531
+ module_spec = module.__spec__
532
+ if module_spec and module_spec.loader:
533
+ module_spec.loader.exec_module(module)
534
+ # Once strawberry is loaded, patch it
535
+ if module.__name__ == "strawberry" and not _is_strawberry_patched:
536
+ patch_strawberry_module(module)
537
+
538
+
539
+ def patch_schema():
540
+ """Patch strawberry.Schema to include both Sailfish and NetworkHop extensions by default."""
541
+ try:
542
+ import strawberry
543
+
544
+ original_schema_init = strawberry.Schema.__init__
545
+
546
+ def patched_schema_init(self, *args, extensions=None, **kwargs):
547
+ if extensions is None:
548
+ extensions = []
549
+
550
+ # Add the custom error handling extension
551
+ sailfish_ext = get_extension()
552
+ if sailfish_ext not in extensions:
553
+ extensions.append(sailfish_ext)
554
+
555
+ # Add the network hop extension
556
+ hop_ext = get_network_hop_extension()
557
+ if hop_ext not in extensions:
558
+ extensions.append(hop_ext)
559
+
560
+ # Call the original constructor
561
+ original_schema_init(self, *args, extensions=extensions, **kwargs)
562
+
563
+ if SF_DEBUG and app_config._interceptors_initialized:
564
+ print(
565
+ "[[DEBUG]] Patched strawberry.Schema to include Sailfish & NetworkHop extensions. [[/DEBUG]]",
566
+ log=False,
567
+ )
568
+
569
+ # Apply the patch
570
+ strawberry.Schema.__init__ = patched_schema_init
571
+
572
+ if SF_DEBUG and app_config._interceptors_initialized:
573
+ print(
574
+ "[[DEBUG]] Successfully patched strawberry.Schema. [[/DEBUG]]",
575
+ log=False,
576
+ )
577
+ except ImportError:
578
+ if SF_DEBUG and app_config._interceptors_initialized:
579
+ print(
580
+ "[[DEBUG]] Strawberry is not installed. Skipping schema patching. [[/DEBUG]]",
581
+ log=False,
582
+ )
583
+
584
+
585
+ def patch_views():
586
+ """
587
+ Patch Strawberry view classes to capture and print request data on errors.
588
+ This helps debug malformed requests when STRAWBERRY_DEBUG is enabled.
589
+ Also transmits exceptions with full stack traces when CAPTURE_STRAWBERRY_ERRORS_WITH_DATA is enabled.
590
+ """
591
+ if not STRAWBERRY_DEBUG and not CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
592
+ return # Skip patching if neither debug mode nor capture mode is enabled
593
+
594
+ try:
595
+ # Try to import Strawberry Django view
596
+ try:
597
+ from strawberry.django.views import GraphQLView as DjangoGraphQLView
598
+
599
+ _patch_view_class(DjangoGraphQLView, "Django")
600
+ except ImportError:
601
+ if SF_DEBUG and app_config._interceptors_initialized:
602
+ print(
603
+ "[[DEBUG]] Strawberry Django view not found. [[/DEBUG]]", log=False
604
+ )
605
+
606
+ # Try to import base async view (used by other integrations)
607
+ try:
608
+ from strawberry.http.async_base_view import AsyncBaseHTTPView
609
+
610
+ _patch_async_base_view(AsyncBaseHTTPView)
611
+ except ImportError:
612
+ if SF_DEBUG and app_config._interceptors_initialized:
613
+ print(
614
+ "[[DEBUG]] Strawberry AsyncBaseHTTPView not found. [[/DEBUG]]",
615
+ log=False,
616
+ )
617
+
618
+ except Exception as e:
619
+ if SF_DEBUG and app_config._interceptors_initialized:
620
+ print(
621
+ f"[[DEBUG]] Failed to patch Strawberry views: {e}. [[/DEBUG]]",
622
+ log=False,
623
+ )
624
+
625
+
626
+ def _patch_view_class(view_class, integration_name):
627
+ """Patch a Strawberry view class to capture request data on errors."""
628
+ if hasattr(view_class, "_sf_patched"):
629
+ return # Already patched
630
+
631
+ original_dispatch = view_class.dispatch
632
+
633
+ async def patched_dispatch(self, request, *args, **kwargs):
634
+ # Capture raw request body before processing
635
+ raw_body = None
636
+ if STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
637
+ try:
638
+ raw_body = request.body if hasattr(request, "body") else None
639
+ except Exception:
640
+ pass
641
+
642
+ try:
643
+ return await original_dispatch(self, request, *args, **kwargs)
644
+ except Exception as e:
645
+ if (
646
+ STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA
647
+ ) and raw_body is not None:
648
+ _print_request_debug_info(raw_body, e, integration_name)
649
+ raise
650
+
651
+ view_class.dispatch = patched_dispatch
652
+ view_class._sf_patched = True
653
+
654
+ if SF_DEBUG and app_config._interceptors_initialized:
655
+ print(
656
+ f"[[DEBUG]] Patched Strawberry {integration_name} view for error debugging. [[/DEBUG]]",
657
+ log=False,
658
+ )
659
+
660
+
661
+ def _patch_async_base_view(view_class):
662
+ """Patch AsyncBaseHTTPView to capture request data on parse errors."""
663
+ if hasattr(view_class, "_sf_parse_patched"):
664
+ return # Already patched
665
+
666
+ original_parse = view_class.parse_http_body
667
+
668
+ async def patched_parse_http_body(self, request_adapter):
669
+ # Capture raw body before parsing (but avoid consuming the stream twice)
670
+ raw_body = None
671
+ if STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
672
+ try:
673
+ # Read the body once
674
+ raw_body = await request_adapter.get_body()
675
+
676
+ # Patch request_adapter.get_body to return cached body
677
+ # (body streams can only be read once)
678
+ async def cached_get_body():
679
+ return raw_body
680
+
681
+ request_adapter.get_body = cached_get_body
682
+ except Exception:
683
+ pass
684
+
685
+ try:
686
+ return await original_parse(self, request_adapter)
687
+ except Exception as e:
688
+ logger.info("=" * 20 + " <STRAWBERRY> " + "=" * 20)
689
+ logger.error(e)
690
+ logger.info("=" * 20 + " </STRAWBERRY> " + "=" * 20)
691
+ if (
692
+ STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA
693
+ ) and raw_body is not None:
694
+ _print_request_debug_info(raw_body, e, "AsyncBaseHTTPView")
695
+ raise
696
+
697
+ view_class.parse_http_body = patched_parse_http_body
698
+ view_class._sf_parse_patched = True
699
+
700
+ if SF_DEBUG and app_config._interceptors_initialized:
701
+ print(
702
+ "[[DEBUG]] Patched Strawberry AsyncBaseHTTPView.parse_http_body for error debugging. [[/DEBUG]]",
703
+ log=False,
704
+ )
705
+
706
+
707
+ def _count_traceback_frames(tb):
708
+ """Count the number of frames in a traceback."""
709
+ count = 0
710
+ while tb is not None:
711
+ count += 1
712
+ tb = tb.tb_next
713
+ return count
714
+
715
+
716
+ def _print_request_debug_info(raw_body, exception, source):
717
+ """Print debug information about the request that caused an error."""
718
+
719
+ # Transmit exception to Sailfish with full stack trace if enabled
720
+ if CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
721
+ try:
722
+ # Verify that the exception has a traceback attached
723
+ if (
724
+ not hasattr(exception, "__traceback__")
725
+ or exception.__traceback__ is None
726
+ ):
727
+ if SF_DEBUG and app_config._interceptors_initialized:
728
+ print(
729
+ f"[[STRAWBERRY_DEBUG]] WARNING: Exception {type(exception).__name__} has no __traceback__ attribute!",
730
+ log=False,
731
+ )
732
+ else:
733
+ if SF_DEBUG and app_config._interceptors_initialized:
734
+ print(
735
+ f"[[STRAWBERRY_DEBUG]] Exception has traceback with {_count_traceback_frames(exception.__traceback__)} frames",
736
+ log=False,
737
+ )
738
+
739
+ transmit_exception_to_sailfish(exception, force_transmit=False)
740
+ if SF_DEBUG and app_config._interceptors_initialized:
741
+ print(
742
+ f"[[STRAWBERRY_DEBUG]] Transmitted exception to Sailfish: {type(exception).__name__}",
743
+ log=False,
744
+ )
745
+ except Exception as transmit_err:
746
+ if SF_DEBUG and app_config._interceptors_initialized:
747
+ print(
748
+ f"[[STRAWBERRY_DEBUG]] Failed to transmit exception: {transmit_err}",
749
+ log=False,
750
+ )
751
+
752
+ print(
753
+ f"[[STRAWBERRY_DEBUG]] Transmission error traceback:\n{traceback.format_exc()}",
754
+ log=False,
755
+ )
756
+
757
+ # Print debug info if STRAWBERRY_DEBUG is enabled
758
+ if not STRAWBERRY_DEBUG:
759
+ return # Skip printing if debug mode is disabled
760
+
761
+ print("\n" + "=" * 80, log=False)
762
+ print(f"[[STRAWBERRY_DEBUG]] Error in {source}", log=False)
763
+ print("=" * 80, log=False)
764
+
765
+ # Print the exception
766
+ print(f"\nException: {type(exception).__name__}: {exception}", log=False)
767
+ print("\nTraceback:", log=False)
768
+ print(traceback.format_exc(), log=False)
769
+
770
+ # Print raw body
771
+ print("\n" + "-" * 80, log=False)
772
+ print("Raw HTTP Body (bytes):", log=False)
773
+ print("-" * 80, log=False)
774
+ if isinstance(raw_body, bytes):
775
+ print(f"Length: {len(raw_body)} bytes", log=False)
776
+ print(f"Raw: {raw_body!r}", log=False)
777
+
778
+ # Try to decode and pretty-print as JSON
779
+ try:
780
+ decoded = raw_body.decode("utf-8")
781
+ print(f"\nDecoded (UTF-8): {decoded}", log=False)
782
+
783
+ # Try to parse as JSON
784
+ try:
785
+ if HAS_ORJSON:
786
+ parsed = orjson.loads(decoded)
787
+ else:
788
+ parsed = json.loads(decoded)
789
+ print(f"\nParsed JSON (type: {type(parsed).__name__}):", log=False)
790
+ if HAS_ORJSON:
791
+ parsed = print(orjson.dumps(parsed, indent=2), log=False)
792
+ else:
793
+ parsed = print(json.dumps(parsed, indent=2), log=False)
794
+ except json.JSONDecodeError as json_err:
795
+ print(f"\nFailed to parse as JSON: {json_err}", log=False)
796
+ except UnicodeDecodeError as decode_err:
797
+ print(f"\nFailed to decode as UTF-8: {decode_err}", log=False)
798
+ else:
799
+ print(f"Body type: {type(raw_body).__name__}", log=False)
800
+ print(f"Body: {raw_body!r}", log=False)
801
+
802
+ print("\n" + "=" * 80, log=False)
803
+ print("[[/STRAWBERRY_DEBUG]]", log=False)
804
+ print("=" * 80 + "\n", log=False)
805
+
806
+
807
+ def patch_strawberry():
808
+ """
809
+ Main entry point for patching Strawberry GraphQL.
810
+ Applies both schema extensions and error debugging patches.
811
+ """
812
+ patch_schema()
813
+ patch_views()