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