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,483 @@
1
+ """
2
+ Instrument urllib.request so that
3
+
4
+ • Every call to urlopen() or OpenerDirector.open() propagates
5
+ SAILFISH tracing headers (unless destination host is excluded).
6
+ • Every call triggers record_network_request(…) UNLESS LD_PRELOAD is active.
7
+
8
+ The patch is safe to import multiple times.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import io
14
+ import os
15
+ import time
16
+ from typing import List, Optional
17
+
18
+ from ...thread_local import trace_id_ctx
19
+
20
+ try:
21
+ import wrapt
22
+
23
+ HAS_WRAPT = True
24
+ except ImportError:
25
+ HAS_WRAPT = False
26
+
27
+ # JSON serialization - try fast orjson first, fallback to stdlib json
28
+ try:
29
+ import orjson
30
+
31
+ HAS_ORJSON = True
32
+ except ImportError:
33
+ import json
34
+
35
+ HAS_ORJSON = False
36
+
37
+ from .utils import (
38
+ init_fast_header_check,
39
+ inject_headers_ultrafast,
40
+ is_ssl_socket_active,
41
+ record_network_request,
42
+ )
43
+
44
+ # ------------------------------- config / helpers --------------------------------- #
45
+
46
+ _SF_REQ_ALREADY_INJECTED_ATTR = "_sf_already_injected"
47
+ _SF_URLLIB_DEBUG = os.getenv("SF_URLLIB_DEBUG", "0") == "1"
48
+ # If true, honor urllib's env proxy logic; if false, we build a proxy-less opener for wire I/O.
49
+ trust_env = os.getenv("SF_URLLIB_TRUST_ENV", "false").lower() == "true"
50
+
51
+
52
+ def _tee_preload_active() -> bool:
53
+ """Detect if LD_PRELOAD tee is active."""
54
+ if os.getenv("SF_TEE_PRELOAD_ONLY", "0") == "1":
55
+ return True
56
+ ld = os.getenv("LD_PRELOAD", "")
57
+ return "libsfnettee.so" in ld or "_sfteepreload" in ld
58
+
59
+
60
+ def _has_header_case_insensitive(req, name: str) -> bool:
61
+ """True if Request already has a header named `name` (case-insensitive)."""
62
+ try:
63
+ items = req.header_items() # type: ignore[attr-defined]
64
+ except Exception:
65
+ try:
66
+ items = list(getattr(req, "headers", {}).items())
67
+ except Exception:
68
+ items = []
69
+ lname = name.lower()
70
+ for k, _ in items:
71
+ if str(k).lower() == lname:
72
+ return True
73
+ return False
74
+
75
+
76
+ class _ResponseTee:
77
+ """
78
+ File-like wrapper for urllib responses that tees bytes into an internal buffer
79
+ as the caller consumes them. On EOF/close, invokes on_complete(buffer_bytes).
80
+ """
81
+
82
+ __slots__ = ("_resp", "_buf", "_cap", "_done", "_on_complete", "_truncated")
83
+
84
+ def __init__(self, resp, on_complete, cap_bytes: int = 256 * 1024):
85
+ self._resp = resp
86
+ self._buf = io.BytesIO()
87
+ self._cap = cap_bytes
88
+ self._done = False
89
+ self._truncated = False
90
+ self._on_complete = on_complete
91
+
92
+ # -------- helpers --------
93
+ def _accumulate(self, chunk: bytes) -> None:
94
+ if not chunk:
95
+ return
96
+ if self._buf.tell() < self._cap:
97
+ remaining = self._cap - self._buf.tell()
98
+ if len(chunk) > remaining:
99
+ self._buf.write(chunk[:remaining])
100
+ self._truncated = True
101
+ else:
102
+ self._buf.write(chunk)
103
+
104
+ def _finish_if_needed(self, reached_eof: bool) -> None:
105
+ if reached_eof and not self._done:
106
+ self._done = True
107
+ try:
108
+ payload = self._buf.getvalue()
109
+ self._on_complete(payload, self._truncated)
110
+ finally:
111
+ self._buf.close()
112
+
113
+ # -------- file-like API --------
114
+ def read(self, size: int = -1) -> bytes:
115
+ data = self._resp.read(size)
116
+ self._accumulate(data)
117
+ self._finish_if_needed(reached_eof=(not data))
118
+ return data
119
+
120
+ def readinto(self, b) -> int:
121
+ n = self._resp.readinto(b)
122
+ if n and n > 0:
123
+ self._accumulate(memoryview(b)[:n].tobytes())
124
+ self._finish_if_needed(reached_eof=(n == 0))
125
+ return n
126
+
127
+ def readline(self, size: int = -1) -> bytes:
128
+ line = self._resp.readline(size)
129
+ self._accumulate(line)
130
+ self._finish_if_needed(reached_eof=(line == b""))
131
+ return line
132
+
133
+ def __iter__(self):
134
+ for line in self._resp:
135
+ self._accumulate(line)
136
+ yield line
137
+ self._finish_if_needed(reached_eof=True)
138
+
139
+ def close(self):
140
+ try:
141
+ self._resp.close()
142
+ finally:
143
+ self._finish_if_needed(reached_eof=True)
144
+
145
+ def __enter__(self):
146
+ """Support context manager protocol."""
147
+ return self
148
+
149
+ def __exit__(self, exc_type, exc_val, exc_tb):
150
+ """Support context manager protocol."""
151
+ self.close()
152
+ return False
153
+
154
+ def __getattr__(self, name):
155
+ return getattr(self._resp, name)
156
+
157
+
158
+ # ------------------------------- patcher --------------------------------- #
159
+
160
+
161
+ def patch_urllib_request(
162
+ domains_to_not_propagate_headers_to: Optional[List[str]] = None,
163
+ ) -> None:
164
+ """
165
+ Apply patches. When LD_PRELOAD is active:
166
+ - ALWAYS inject headers (trace_id + funcspan_override)
167
+ - SKIP capture/emission (LD_PRELOAD handles at socket layer)
168
+ """
169
+ try:
170
+ import socket as _socket # for _GLOBAL_DEFAULT_TIMEOUT
171
+ import urllib.error
172
+ import urllib.request as _ur
173
+ except ImportError:
174
+ return
175
+
176
+ exclude: List[str] = domains_to_not_propagate_headers_to or []
177
+ preload_active = _tee_preload_active()
178
+
179
+ # Initialize C extension fast check once when LD_PRELOAD is active
180
+ if preload_active:
181
+ init_fast_header_check(exclude)
182
+
183
+ _orig_urlopen = _ur.urlopen
184
+ _orig_opener_open = _ur.OpenerDirector.open # type: ignore[attr-defined]
185
+
186
+ # -------- internal helpers (no recursion!) --------
187
+
188
+ def _ensure_content_length_semantics(req: _ur.Request) -> None:
189
+ """
190
+ Guarantee standards-compliant body semantics:
191
+
192
+ - For POST/PUT/PATCH/DELETE/OPTIONS with no body: send Content-Length: 0
193
+ - For HEAD: no body
194
+ - Also ensure a benign Content-Type for zero-length bodies on body-capable verbs.
195
+ Some stacks 400 when Content-Type is missing on e.g. PUT with empty body.
196
+ """
197
+ method = req.get_method()
198
+
199
+ if method == "HEAD":
200
+ # HEAD must not have a body.
201
+ return
202
+
203
+ body_capable = {"POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
204
+
205
+ if method in body_capable:
206
+ has_body = getattr(req, "data", None) is not None
207
+
208
+ if not has_body:
209
+ # Prefer setting an empty body so urllib emits Content-Length: 0.
210
+ try:
211
+ req.data = b""
212
+ has_body = True
213
+ except Exception:
214
+ # Fallback to explicit header only.
215
+ if not _has_header_case_insensitive(req, "Content-Length"):
216
+ try:
217
+ req.add_header("Content-Length", "0")
218
+ except Exception:
219
+ pass
220
+
221
+ # Make sure a benign Content-Type exists for empty bodies on these verbs.
222
+ # urllib adds a default Content-Type for POST, but not always for others.
223
+ if not _has_header_case_insensitive(req, "Content-Type"):
224
+ try:
225
+ # Use application/octet-stream to avoid implying form encoding.
226
+ req.add_header("Content-Type", "application/octet-stream")
227
+ except Exception:
228
+ pass
229
+
230
+ def _maybe_inject_headers(req: _ur.Request) -> None:
231
+ if getattr(req, _SF_REQ_ALREADY_INJECTED_ATTR, False):
232
+ return
233
+ headers_dict = dict(req.headers)
234
+ inject_headers_ultrafast(headers_dict, req.full_url, exclude)
235
+ for k, v in headers_dict.items():
236
+ if not _has_header_case_insensitive(req, k):
237
+ req.add_header(k, v)
238
+ setattr(req, _SF_REQ_ALREADY_INJECTED_ATTR, True)
239
+
240
+ def _proxyless_open(req: _ur.Request, timeout):
241
+ """Open using a proxy-less opener, calling the ORIGINAL .open to avoid re-entry."""
242
+ if trust_env:
243
+ # Honor env proxies/config via the original urlopen
244
+ return _orig_urlopen(req, timeout=timeout)
245
+ opener = _ur.build_opener(_ur.ProxyHandler({}))
246
+ return _orig_opener_open(opener, req, timeout=timeout)
247
+
248
+ # ------------------------------------------------------------------ #
249
+ # Core helper used by both urlopen and OpenerDirector.open
250
+ # ------------------------------------------------------------------ #
251
+ def _inject_and_record(
252
+ opener_call, # callable(req, timeout=?)
253
+ req_or_url,
254
+ data,
255
+ timeout,
256
+ ):
257
+ # 1) Normalize to a Request object (no duplicate 'data' passing later)
258
+ if isinstance(req_or_url, _ur.Request):
259
+ req = req_or_url
260
+ else:
261
+ req = _ur.Request(req_or_url, data=data)
262
+
263
+ # 2) Header injection + body semantics (single pass)
264
+ _maybe_inject_headers(req)
265
+ _ensure_content_length_semantics(req)
266
+ method = req.get_method()
267
+
268
+ # 3) Trace id for capture (skip when LD_PRELOAD active)
269
+ if not preload_active:
270
+ trace_id = trace_id_ctx.get(None) or ""
271
+ else:
272
+ trace_id = ""
273
+
274
+ # 4) Serialize request headers/data for capture
275
+ req_data = b""
276
+ req_headers = b""
277
+ try:
278
+ if getattr(req, "data", None):
279
+ if isinstance(req.data, bytes):
280
+ req_data = req.data
281
+ elif isinstance(req.data, str):
282
+ req_data = req.data.encode("utf-8")
283
+ if HAS_ORJSON:
284
+ req_headers = orjson.dumps({str(k): str(v) for k, v in req.headers.items()})
285
+ else:
286
+ req_headers = json.dumps({str(k): str(v) for k, v in req.headers.items()}).encode("utf-8")
287
+ except Exception:
288
+ pass
289
+
290
+ # 5) Perform I/O
291
+ t0 = int(time.time() * 1_000)
292
+ try:
293
+ resp = opener_call(req, timeout=timeout)
294
+ status = (
295
+ getattr(resp, "status", None) or getattr(resp, "getcode", lambda: 0)()
296
+ )
297
+ success = status < 400
298
+
299
+ if _SF_URLLIB_DEBUG:
300
+ try:
301
+ print(
302
+ f"[SF urllib] {method} {req.full_url} -> {status}", flush=True
303
+ )
304
+ except Exception:
305
+ pass
306
+
307
+ if HAS_ORJSON:
308
+ resp_headers = orjson.dumps({str(k): str(v) for k, v in resp.headers.items()})
309
+ else:
310
+ resp_headers = json.dumps({str(k): str(v) for k, v in resp.headers.items()}).encode("utf-8")
311
+
312
+ if preload_active:
313
+ return resp
314
+
315
+ # Skip capture for HTTPS when ssl_socket.py is active (avoids double-capture)
316
+ is_https = req.full_url.startswith("https://")
317
+ if is_https and is_ssl_socket_active():
318
+ return resp
319
+
320
+ def _on_complete(resp_bytes: bytes, _truncated: bool):
321
+ record_network_request(
322
+ trace_id,
323
+ req.full_url,
324
+ method,
325
+ status,
326
+ success,
327
+ None,
328
+ timestamp_start=t0,
329
+ timestamp_end=int(time.time() * 1_000),
330
+ request_data=req_data,
331
+ response_data=resp_bytes,
332
+ request_headers=req_headers,
333
+ response_headers=resp_headers,
334
+ )
335
+
336
+ cap = int(os.getenv("SF_URLOPEN_CAPTURE_CAP_BYTES", "262144"))
337
+ return _ResponseTee(resp, _on_complete, cap_bytes=cap)
338
+
339
+ except urllib.error.HTTPError as e:
340
+ # 4xx/5xx → exception; capture and re-raise
341
+ if _SF_URLLIB_DEBUG:
342
+ try:
343
+ print(
344
+ f"[SF urllib] {req.get_method()} {req.full_url} -> {getattr(e, 'code', 0)} (HTTPError)",
345
+ flush=True,
346
+ )
347
+ except Exception:
348
+ pass
349
+
350
+ if HAS_ORJSON:
351
+ resp_headers = orjson.dumps({str(k): str(v) for k, v in e.headers.items()})
352
+ else:
353
+ resp_headers = json.dumps({str(k): str(v) for k, v in e.headers.items()}).encode("utf-8")
354
+
355
+ if not preload_active:
356
+ body = b""
357
+ try:
358
+ cap = int(os.getenv("SF_URLOPEN_CAPTURE_CAP_BYTES", "262144"))
359
+ body = e.read()
360
+ if len(body) > cap:
361
+ body = body[:cap]
362
+ # Put body back so downstream can still read it
363
+ e.fp = io.BytesIO(body)
364
+ except Exception:
365
+ pass
366
+
367
+ record_network_request(
368
+ trace_id,
369
+ req.full_url,
370
+ req.get_method(),
371
+ getattr(e, "code", 0) or 0,
372
+ False,
373
+ str(e),
374
+ timestamp_start=t0,
375
+ timestamp_end=int(time.time() * 1_000),
376
+ request_data=req_data,
377
+ response_data=body,
378
+ request_headers=req_headers,
379
+ response_headers=resp_headers,
380
+ )
381
+ raise
382
+
383
+ except Exception as e:
384
+ if _SF_URLLIB_DEBUG:
385
+ try:
386
+ print(
387
+ f"[SF urllib] {req.get_method()} {req.full_url} -> exception: {e}",
388
+ flush=True,
389
+ )
390
+ except Exception:
391
+ pass
392
+ if not preload_active:
393
+ record_network_request(
394
+ trace_id,
395
+ req.full_url,
396
+ req.get_method(),
397
+ 0,
398
+ False,
399
+ str(e)[:255],
400
+ timestamp_start=t0,
401
+ timestamp_end=int(time.time() * 1_000),
402
+ request_data=req_data,
403
+ request_headers=req_headers,
404
+ )
405
+ raise
406
+
407
+ # ------------------------------------------------------------------ #
408
+ # Module-level urlopen patch
409
+ # ------------------------------------------------------------------ #
410
+ if HAS_WRAPT:
411
+
412
+ def instrumented_urlopen(wrapped, instance, args, kwargs):
413
+ # urlopen(url, data=None, timeout=..., *, cafile=..., capath=..., cadefault=..., context=...)
414
+ url = args[0] if len(args) > 0 else kwargs.pop("url", "")
415
+ data = args[1] if len(args) > 1 else kwargs.pop("data", None)
416
+
417
+ if len(args) > 2:
418
+ timeout = args[2]
419
+ else:
420
+ timeout = kwargs.pop("timeout", _ur.socket._GLOBAL_DEFAULT_TIMEOUT) # type: ignore
421
+
422
+ # We pass a callable that avoids proxies unless trust_env is set
423
+ return _inject_and_record(_proxyless_open, url, data, timeout)
424
+
425
+ wrapt.wrap_function_wrapper("urllib.request", "urlopen", instrumented_urlopen)
426
+ else:
427
+
428
+ def patched_urlopen(url, data=None, timeout=None, *a, **kw): # type: ignore
429
+ if "timeout" in kw:
430
+ timeout = kw.pop("timeout")
431
+ if "data" in kw:
432
+ data = kw.pop("data")
433
+ return _inject_and_record(_proxyless_open, url, data, timeout)
434
+
435
+ _ur.urlopen = patched_urlopen # type: ignore[assignment]
436
+
437
+ # ------------------------------------------------------------------ #
438
+ # OpenerDirector.open patch (covers build_opener, install_opener, etc.)
439
+ # ------------------------------------------------------------------ #
440
+ if HAS_WRAPT:
441
+
442
+ def instrumented_opener_open(wrapped, instance, args, kwargs):
443
+ # Signature: open(self, fullurl, data=None, timeout=None)
444
+ fullurl = args[0] if len(args) > 0 else kwargs.pop("fullurl", "")
445
+ data = args[1] if len(args) > 1 else kwargs.pop("data", None)
446
+ timeout = args[2] if len(args) > 2 else kwargs.pop("timeout", None)
447
+
448
+ # If caller passed a Request that we already injected, short-circuit:
449
+ if isinstance(fullurl, _ur.Request) and getattr(
450
+ fullurl, _SF_REQ_ALREADY_INJECTED_ATTR, False
451
+ ):
452
+ if trust_env:
453
+ # Delegate to wrapped opener respecting env/proxies
454
+ return wrapped(fullurl, timeout=timeout)
455
+ # Use proxy-less opener BUT call the ORIGINAL .open to avoid re-entry
456
+ opener = _ur.build_opener(_ur.ProxyHandler({}))
457
+ return _orig_opener_open(opener, fullurl, timeout=timeout)
458
+
459
+ # Otherwise, flow through our injector and open without proxies (by default)
460
+ return _inject_and_record(_proxyless_open, fullurl, data, timeout)
461
+
462
+ wrapt.wrap_function_wrapper(
463
+ _ur.OpenerDirector, "open", instrumented_opener_open
464
+ )
465
+ else:
466
+
467
+ def patched_opener_open(self, fullurl, data=None, timeout=None, *a, **kw): # type: ignore[override]
468
+ if "timeout" in kw:
469
+ timeout = kw.pop("timeout")
470
+ if "data" in kw:
471
+ data = kw.pop("data")
472
+
473
+ if isinstance(fullurl, _ur.Request) and getattr(
474
+ fullurl, _SF_REQ_ALREADY_INJECTED_ATTR, False
475
+ ):
476
+ if trust_env:
477
+ return _orig_opener_open(self, fullurl, timeout=timeout)
478
+ opener = _ur.build_opener(_ur.ProxyHandler({}))
479
+ return _orig_opener_open(opener, fullurl, timeout=timeout)
480
+
481
+ return _inject_and_record(_proxyless_open, fullurl, data, timeout)
482
+
483
+ _ur.OpenerDirector.open = patched_opener_open # type: ignore[assignment]