weft-kernel 0.1.0__py3-none-any.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.
@@ -0,0 +1,178 @@
1
+ """The Weft kernel.
2
+
3
+ What belongs here is settled in G1 and specified in `docs/01-high-level-plan.md`
4
+ under *The kernel boundary*: the kernel is what is required to express, load and
5
+ run contracts it knows nothing about, plus the domain types those signatures
6
+ unavoidably name — and nothing in the kernel performs RAG work.
7
+
8
+ Its two dependencies, `pydantic` and `opentelemetry-api`, are declared and
9
+ enforced by fitness function 1.
10
+ """
11
+
12
+ from weft_kernel.blocking import BlockingCallError
13
+ from weft_kernel.context import (
14
+ Context,
15
+ DuplicateServiceError,
16
+ ServiceRegistry,
17
+ ServiceRole,
18
+ UnresolvedServiceError,
19
+ )
20
+ from weft_kernel.discovery import (
21
+ ENTRY_POINT_GROUP,
22
+ Disclosure,
23
+ EntryPointLike,
24
+ EnvInterpolationError,
25
+ PackRegistrar,
26
+ PackReport,
27
+ PackSettingsError,
28
+ PackStatus,
29
+ PipelineResource,
30
+ allow_list_from_config,
31
+ discover,
32
+ interpolate_env,
33
+ )
34
+ from weft_kernel.errors import UnresolvedNameError, WeftError
35
+ from weft_kernel.fallback import Attempt, try_in_order
36
+ from weft_kernel.payload import (
37
+ SCHEMA_VERSION_KEY,
38
+ ExtMap,
39
+ ExtModel,
40
+ Failed,
41
+ Lineage,
42
+ MediaType,
43
+ Node,
44
+ NodeId,
45
+ NothingToProduce,
46
+ Outcome,
47
+ Produced,
48
+ Property,
49
+ SchemaVersionRefusedError,
50
+ SourceId,
51
+ SyntheticOrigin,
52
+ Vector,
53
+ )
54
+ from weft_kernel.pipeline import (
55
+ InsertOperator,
56
+ Pipeline,
57
+ SetOperator,
58
+ SlotDeclaration,
59
+ StageDeclaration,
60
+ )
61
+ from weft_kernel.registry import (
62
+ DuplicateRegistrationError,
63
+ MissingDestroysDeclarationError,
64
+ MissingRequiredDeclarationError,
65
+ Registry,
66
+ RegistryEntry,
67
+ UnknownPluginError,
68
+ )
69
+ from weft_kernel.resolution import (
70
+ Contribution,
71
+ InvalidStageConfigError,
72
+ OperatorIdCollisionError,
73
+ PipelineCycleError,
74
+ ResolvedPipeline,
75
+ ResolvedStage,
76
+ SlotOrderConflictError,
77
+ StageNotConfigurableError,
78
+ StaleOperatorTargetError,
79
+ UndefinedVarError,
80
+ UnknownParentPipelineError,
81
+ resolve,
82
+ )
83
+ from weft_kernel.runner import (
84
+ FlushError,
85
+ IntactViolationError,
86
+ Lifetime,
87
+ PipelineResolutionError,
88
+ RunnablePipeline,
89
+ Runner,
90
+ RunSummary,
91
+ Stage,
92
+ StageCompositionError,
93
+ StageSpec,
94
+ TenantMismatchError,
95
+ UnknownFallbackError,
96
+ UnmetRequiresError,
97
+ )
98
+ from weft_kernel.seam import Deprecation, wrap, wrap_flush
99
+
100
+ __all__ = [
101
+ "ENTRY_POINT_GROUP",
102
+ "SCHEMA_VERSION_KEY",
103
+ "Attempt",
104
+ "BlockingCallError",
105
+ "Context",
106
+ "Contribution",
107
+ "Deprecation",
108
+ "Disclosure",
109
+ "DuplicateRegistrationError",
110
+ "DuplicateServiceError",
111
+ "EntryPointLike",
112
+ "EnvInterpolationError",
113
+ "ExtMap",
114
+ "ExtModel",
115
+ "Failed",
116
+ "FlushError",
117
+ "InsertOperator",
118
+ "IntactViolationError",
119
+ "InvalidStageConfigError",
120
+ "Lifetime",
121
+ "Lineage",
122
+ "MediaType",
123
+ "MissingDestroysDeclarationError",
124
+ "MissingRequiredDeclarationError",
125
+ "Node",
126
+ "NodeId",
127
+ "NothingToProduce",
128
+ "OperatorIdCollisionError",
129
+ "Outcome",
130
+ "PackRegistrar",
131
+ "PackReport",
132
+ "PackSettingsError",
133
+ "PackStatus",
134
+ "Pipeline",
135
+ "PipelineCycleError",
136
+ "PipelineResolutionError",
137
+ "PipelineResource",
138
+ "Produced",
139
+ "Property",
140
+ "Registry",
141
+ "RegistryEntry",
142
+ "ResolvedPipeline",
143
+ "ResolvedStage",
144
+ "RunnablePipeline",
145
+ "Runner",
146
+ "RunSummary",
147
+ "SchemaVersionRefusedError",
148
+ "ServiceRegistry",
149
+ "ServiceRole",
150
+ "SetOperator",
151
+ "SlotDeclaration",
152
+ "SlotOrderConflictError",
153
+ "SourceId",
154
+ "Stage",
155
+ "StageCompositionError",
156
+ "StageDeclaration",
157
+ "StageNotConfigurableError",
158
+ "StageSpec",
159
+ "StaleOperatorTargetError",
160
+ "SyntheticOrigin",
161
+ "TenantMismatchError",
162
+ "UndefinedVarError",
163
+ "UnknownFallbackError",
164
+ "UnknownParentPipelineError",
165
+ "UnknownPluginError",
166
+ "UnmetRequiresError",
167
+ "UnresolvedNameError",
168
+ "UnresolvedServiceError",
169
+ "Vector",
170
+ "WeftError",
171
+ "allow_list_from_config",
172
+ "discover",
173
+ "interpolate_env",
174
+ "resolve",
175
+ "try_in_order",
176
+ "wrap",
177
+ "wrap_flush",
178
+ ]
@@ -0,0 +1,370 @@
1
+ """The categorical blocking-call detector — fitness function 7(b).
2
+
3
+ Specified in `docs/01-high-level-plan.md` → *Fitness functions*, clause 7(b),
4
+ and `docs/02-extension-model.md` → *Colour*: while a stage runs, a detector
5
+ installed at the registration seam fails the build on a blocking call made on
6
+ the event loop thread — file IO, sockets, `time.sleep`, `subprocess`, and (for
7
+ free, because most of them go through a socket to do it) a synchronous
8
+ database driver. **Categorical, not a threshold.** The rejected alternative
9
+ was a slow-callback duration constant; it was rejected because its correct
10
+ value is machine-dependent and changes legitimately with every runner and
11
+ dependency bump, so it cannot be ratcheted the way the kernel budget can.
12
+ There is nothing to tune here: a call either happened on the loop thread
13
+ during a stage's `run`, or it did not.
14
+
15
+ **Detecting the operation, not the object.** A `socket.socket()` call
16
+ constructs an object; it does not block. What blocks is a *use* of that
17
+ object in blocking mode — `connect`, `recv`, `recv_into`, `send`, `sendall`
18
+ or `accept` called on a socket whose timeout is not `0`. `asyncio`'s own
19
+ `BaseEventLoop._connect_sock` builds every outbound connection's socket via
20
+ this same constructor and then immediately calls `sock.setblocking(False)`
21
+ (equivalent to `settimeout(0)`) before touching it — so a detector that
22
+ fired on construction would flag every legitimate async HTTP call, embedding
23
+ call and vector-store round trip a stage makes, which is exactly the
24
+ traffic `01`'s *Colour* section describes as "invisible to it by
25
+ construction". Checking the socket's own `gettimeout()` at the moment of the
26
+ operation keeps the detector categorical rather than reintroducing a
27
+ threshold: a socket is either in blocking mode or it is not, and Python
28
+ already tracks which.
29
+
30
+ The same six operations, plus `accept`, are patched a second time on
31
+ `ssl.SSLSocket`. `ssl.SSLSocket` overrides `connect`, `recv`, `recv_into`,
32
+ `send`, `sendall` and `accept` rather than inheriting `socket.socket`'s —
33
+ its read/write path runs through the TLS layer (`self._sslobj`), not the
34
+ plain-socket implementation the patches above replace — so a synchronous
35
+ HTTPS client (`requests`, a sync `httpx.Client`, most sync vendor SDKs)
36
+ would otherwise be caught only at `connect` (which does route through
37
+ `socket.socket.connect`) and never at the read or write that actually
38
+ stalls the loop. `ssl` is imported lazily, inside `_install`, so the kernel
39
+ does not pay the interpreter's TLS/OpenSSL binding cost at import time for
40
+ a stage that never makes a network call.
41
+
42
+ The same distinction holds for `subprocess`. `subprocess.Popen(...)`
43
+ constructs an object; it does not block — it forks/execs and returns. What
44
+ blocks is a *use* of that object: `Popen.wait()` or `Popen.communicate()`.
45
+ `asyncio`'s own `_UnixSubprocessTransport._start` (the machinery behind
46
+ `await asyncio.create_subprocess_exec(...)`, the only correct way to run a
47
+ subprocess from async code) builds the child via this same constructor on
48
+ the event loop thread — so a detector that fired on construction would
49
+ raise inside every correct, awaited subprocess call a stage makes. Patching
50
+ `wait`/`communicate` instead loses no real detection: `subprocess.run`,
51
+ `check_output` and `check_call` all route through `communicate` then `wait`,
52
+ so every synchronous convenience wrapper is still caught unconditionally —
53
+ there is no timeout or blocking-mode flag to check first, because these two
54
+ calls block by definition. `asyncio.subprocess.Process.wait` goes through
55
+ the transport and its child watcher, never through `Popen.wait`, so the
56
+ awaited path never reaches the patch.
57
+
58
+ **Scope, and why it does not false-positive on the sanctioned escape hatch.**
59
+ `guard()` arms a `ContextVar` for the duration of one stage's `run` call,
60
+ recording the stage's label and the identifier of the thread running the
61
+ event loop at that moment. Several stdlib entry points are patched, once, to
62
+ consult it:
63
+
64
+ - `ContextVar` is copied per `asyncio.Task`, never shared, so a *second*,
65
+ unrelated pipeline running concurrently on the same loop carries its own
66
+ (unarmed) copy and is untouched — this is what "scoped to stage execution"
67
+ means at run time, not merely at the source level.
68
+ - `asyncio.to_thread` propagates the current `Context` into its worker thread
69
+ by design, so the var *is* still armed there — but the thread id captured
70
+ at `guard()`'s entry no longer matches `threading.get_ident()` inside the
71
+ worker, so a call a stage deliberately offloaded is not flagged. That is
72
+ the sanctioned way to make a blocking call, per `01` → *Colour*, and it is
73
+ excluded by construction rather than by an author remembering to mark it.
74
+ - Fixtures, imports and model downloads run before `guard()` is entered or
75
+ after it exits, so they were never in scope to begin with — `01` states
76
+ this as a design property of the seam, not a claim this module has to
77
+ enforce.
78
+
79
+ **What this does not, and must not, become.** `docs/02-extension-model.md`
80
+ names the trap directly: this detector sees only *blocking* calls on the loop
81
+ thread. An async HTTP client, or anything already behind `to_thread`, is
82
+ invisible to it by construction — sound for colour, unsound for security. It
83
+ must never be reused as a network- or filesystem-access observer.
84
+ """
85
+
86
+ from __future__ import annotations
87
+
88
+ import builtins
89
+ import io
90
+ import socket
91
+ import subprocess
92
+ import threading
93
+ import time
94
+ from collections.abc import Callable, Generator
95
+ from contextlib import contextmanager
96
+ from contextvars import ContextVar
97
+ from dataclasses import dataclass
98
+ from typing import TYPE_CHECKING, Any
99
+
100
+ from weft_kernel.errors import WeftError
101
+
102
+ if TYPE_CHECKING:
103
+ import ssl
104
+
105
+
106
+ class BlockingCallError(WeftError):
107
+ """A stage made a blocking call on the event loop thread. Fitness function 7(b).
108
+
109
+ Raised from inside the blocking call itself — `open`, a socket operation,
110
+ `time.sleep`, or `Popen.wait`/`Popen.communicate` — so the traceback
111
+ points at the exact call site, not merely at the stage that eventually
112
+ made it.
113
+ """
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class _Watch:
118
+ """What one armed `guard()` window remembers, for the patched functions to consult."""
119
+
120
+ stage: str
121
+ loop_thread_id: int
122
+
123
+
124
+ _watch: ContextVar[_Watch | None] = ContextVar("weft_kernel_blocking_watch", default=None)
125
+
126
+ _installed = False
127
+
128
+ _real_open: Callable[..., Any] = builtins.open
129
+ _real_io_open: Callable[..., Any] = io.open
130
+ _real_sleep: Callable[..., None] = time.sleep
131
+ _real_popen_wait: Callable[..., int] = subprocess.Popen[Any].wait
132
+ _real_popen_communicate: Callable[..., tuple[Any, Any]] = subprocess.Popen[Any].communicate
133
+ _real_socket_connect: Callable[..., Any] = socket.socket.connect
134
+ _real_socket_recv: Callable[..., Any] = socket.socket.recv
135
+ _real_socket_recv_into: Callable[..., Any] = socket.socket.recv_into
136
+ _real_socket_send: Callable[..., Any] = socket.socket.send
137
+ _real_socket_sendall: Callable[..., Any] = socket.socket.sendall
138
+ _real_socket_accept: Callable[..., Any] = socket.socket.accept
139
+
140
+
141
+ def _ssl_patch_not_installed(*_args: Any, **_kwargs: Any) -> Any:
142
+ msg = "ssl.SSLSocket patch not installed"
143
+ raise RuntimeError(msg)
144
+
145
+
146
+ _real_ssl_socket_connect: Callable[..., Any] = _ssl_patch_not_installed
147
+ _real_ssl_socket_recv: Callable[..., Any] = _ssl_patch_not_installed
148
+ _real_ssl_socket_recv_into: Callable[..., Any] = _ssl_patch_not_installed
149
+ _real_ssl_socket_send: Callable[..., Any] = _ssl_patch_not_installed
150
+ _real_ssl_socket_sendall: Callable[..., Any] = _ssl_patch_not_installed
151
+ _real_ssl_socket_accept: Callable[..., Any] = _ssl_patch_not_installed
152
+
153
+
154
+ @contextmanager
155
+ def guard(stage: str) -> Generator[None]:
156
+ """Scope the detector to `stage`'s execution, for the lifetime of this `with` block.
157
+
158
+ Installs the stdlib patches on first use (idempotent — a second `guard()`
159
+ call, nested or sequential, does not re-patch), then arms the watch for
160
+ this task on this thread. Reset on the way out, including on an
161
+ exception: `guard` never decides whether a stage failed, it only decides
162
+ whether a call it made was blocking.
163
+ """
164
+ _install()
165
+ token = _watch.set(_Watch(stage=stage, loop_thread_id=threading.get_ident()))
166
+ try:
167
+ yield
168
+ finally:
169
+ _watch.reset(token)
170
+
171
+
172
+ def _install() -> None:
173
+ """Patch the stdlib's blocking entry points, once per process.
174
+
175
+ `builtins.open` and `io.open` are patched separately because they are
176
+ distinct callables — `pathlib.Path.read_text`/`read_bytes` route through
177
+ `io.open`, not `builtins.open`, and this repository's own ruff `PTH`
178
+ rules push authors toward `Path`, so the file-IO clause of 7(b) would be
179
+ inert for the idiom this codebase mandates if only `builtins.open` were
180
+ covered.
181
+
182
+ The socket entry points are the six *operations* that can block —
183
+ `connect`, `recv`, `recv_into`, `send`, `sendall`, `accept` — patched as
184
+ bound methods on `socket.socket` itself, not by substituting the class.
185
+ Substituting the class would make `isinstance(x, socket.socket)` false
186
+ for objects built through the real constructor, which `asyncio` and
187
+ `ssl` both rely on; patching methods in place leaves class identity, and
188
+ therefore `isinstance`, untouched.
189
+
190
+ The subprocess entry points are, by the same reasoning, the two
191
+ *operations* that can block — `Popen.wait`, `Popen.communicate` — not
192
+ `Popen.__init__`. Construction only forks/execs; `await
193
+ asyncio.create_subprocess_exec(...)` calls it on the event loop thread by
194
+ design (`_UnixSubprocessTransport._start`), so patching the constructor
195
+ would flag every correct, awaited subprocess call a stage makes. `wait`
196
+ and `communicate` block unconditionally when called synchronously — there
197
+ is no non-blocking mode to check for a `Popen`, unlike a socket's
198
+ `gettimeout()` — so both patches call `_check` on every invocation while
199
+ armed.
200
+
201
+ `ssl` is imported here, lazily, and its `SSLSocket` gets the same six
202
+ operations patched a second time — `connect`, `recv`, `recv_into`,
203
+ `send`, `sendall`, `accept` — because `SSLSocket` overrides all six
204
+ rather than inheriting `socket.socket`'s, so the patches above never see
205
+ a TLS socket's read or write. Guarded by the same `gettimeout() != 0`
206
+ check: a non-blocking TLS socket is exactly as exempt as a non-blocking
207
+ plain one.
208
+
209
+ Every patch is a pass-through outside an armed `guard()` window, and for
210
+ a socket, also a pass-through whenever the socket is not in blocking mode
211
+ (`gettimeout() == 0`) — nothing about their behaviour changes for code
212
+ that never runs as a stage, or for a socket already opted into async use.
213
+ This is what makes eager, process-wide installation safe: the gate this
214
+ function installs defaults to open.
215
+ """
216
+ global _installed
217
+ global _real_ssl_socket_connect, _real_ssl_socket_recv, _real_ssl_socket_recv_into
218
+ global _real_ssl_socket_send, _real_ssl_socket_sendall, _real_ssl_socket_accept
219
+ if _installed:
220
+ return
221
+ builtins.open = _guarded_open
222
+ io.open = _guarded_io_open
223
+ time.sleep = _guarded_sleep
224
+ subprocess.Popen.wait = _guarded_popen_wait
225
+ subprocess.Popen.communicate = _guarded_popen_communicate
226
+ socket.socket.connect = _guarded_socket_connect
227
+ socket.socket.recv = _guarded_socket_recv
228
+ socket.socket.recv_into = _guarded_socket_recv_into
229
+ socket.socket.send = _guarded_socket_send
230
+ socket.socket.sendall = _guarded_socket_sendall
231
+ socket.socket.accept = _guarded_socket_accept
232
+
233
+ import ssl
234
+
235
+ _real_ssl_socket_connect = ssl.SSLSocket.connect
236
+ _real_ssl_socket_recv = ssl.SSLSocket.recv
237
+ _real_ssl_socket_recv_into = ssl.SSLSocket.recv_into
238
+ _real_ssl_socket_send = ssl.SSLSocket.send
239
+ _real_ssl_socket_sendall = ssl.SSLSocket.sendall
240
+ _real_ssl_socket_accept = ssl.SSLSocket.accept
241
+ ssl.SSLSocket.connect = _guarded_ssl_socket_connect
242
+ ssl.SSLSocket.recv = _guarded_ssl_socket_recv
243
+ ssl.SSLSocket.recv_into = _guarded_ssl_socket_recv_into
244
+ ssl.SSLSocket.send = _guarded_ssl_socket_send
245
+ ssl.SSLSocket.sendall = _guarded_ssl_socket_sendall
246
+ ssl.SSLSocket.accept = _guarded_ssl_socket_accept
247
+
248
+ _installed = True
249
+
250
+
251
+ def _check(call: str) -> None:
252
+ watch = _watch.get()
253
+ if watch is None or watch.loop_thread_id != threading.get_ident():
254
+ return
255
+ raise BlockingCallError(
256
+ f"stage '{watch.stage}' made a blocking call ({call}) on the event loop thread. "
257
+ f"Offload it — `await asyncio.to_thread(...)` — or use an async client instead. "
258
+ f"See fitness function 7(b), docs/01-high-level-plan.md.",
259
+ stage=watch.stage,
260
+ )
261
+
262
+
263
+ def _guarded_open(*args: Any, **kwargs: Any) -> Any:
264
+ _check("open()")
265
+ return _real_open(*args, **kwargs)
266
+
267
+
268
+ def _guarded_io_open(*args: Any, **kwargs: Any) -> Any:
269
+ _check("open()")
270
+ return _real_io_open(*args, **kwargs)
271
+
272
+
273
+ def _guarded_sleep(*args: Any, **kwargs: Any) -> None:
274
+ _check("time.sleep()")
275
+ _real_sleep(*args, **kwargs)
276
+
277
+
278
+ def _guarded_popen_wait(self: subprocess.Popen[Any], *args: Any, **kwargs: Any) -> int:
279
+ _check("subprocess.wait()")
280
+ return _real_popen_wait(self, *args, **kwargs)
281
+
282
+
283
+ def _guarded_popen_communicate(
284
+ self: subprocess.Popen[Any], *args: Any, **kwargs: Any
285
+ ) -> tuple[Any, Any]:
286
+ _check("subprocess.communicate()")
287
+ return _real_popen_communicate(self, *args, **kwargs)
288
+
289
+
290
+ def _is_blocking(sock: socket.socket) -> bool:
291
+ """A socket is in blocking mode unless it opted out with `settimeout(0)`.
292
+
293
+ `gettimeout()` returns `None` for the default blocking mode and a
294
+ `float` timeout for both the "blocking with a deadline" and the
295
+ non-blocking (`0`) modes — so the only value that means "this socket
296
+ will not block the loop thread" is exactly `0`.
297
+ """
298
+ return sock.gettimeout() != 0
299
+
300
+
301
+ def _guarded_socket_connect(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
302
+ if _is_blocking(self):
303
+ _check("socket.connect()")
304
+ return _real_socket_connect(self, *args, **kwargs)
305
+
306
+
307
+ def _guarded_socket_recv(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
308
+ if _is_blocking(self):
309
+ _check("socket.recv()")
310
+ return _real_socket_recv(self, *args, **kwargs)
311
+
312
+
313
+ def _guarded_socket_recv_into(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
314
+ if _is_blocking(self):
315
+ _check("socket.recv_into()")
316
+ return _real_socket_recv_into(self, *args, **kwargs)
317
+
318
+
319
+ def _guarded_socket_send(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
320
+ if _is_blocking(self):
321
+ _check("socket.send()")
322
+ return _real_socket_send(self, *args, **kwargs)
323
+
324
+
325
+ def _guarded_socket_sendall(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
326
+ if _is_blocking(self):
327
+ _check("socket.sendall()")
328
+ return _real_socket_sendall(self, *args, **kwargs)
329
+
330
+
331
+ def _guarded_socket_accept(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
332
+ if _is_blocking(self):
333
+ _check("socket.accept()")
334
+ return _real_socket_accept(self, *args, **kwargs)
335
+
336
+
337
+ def _guarded_ssl_socket_connect(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
338
+ if _is_blocking(self):
339
+ _check("ssl.SSLSocket.connect()")
340
+ return _real_ssl_socket_connect(self, *args, **kwargs)
341
+
342
+
343
+ def _guarded_ssl_socket_recv(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
344
+ if _is_blocking(self):
345
+ _check("ssl.SSLSocket.recv()")
346
+ return _real_ssl_socket_recv(self, *args, **kwargs)
347
+
348
+
349
+ def _guarded_ssl_socket_recv_into(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
350
+ if _is_blocking(self):
351
+ _check("ssl.SSLSocket.recv_into()")
352
+ return _real_ssl_socket_recv_into(self, *args, **kwargs)
353
+
354
+
355
+ def _guarded_ssl_socket_send(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
356
+ if _is_blocking(self):
357
+ _check("ssl.SSLSocket.send()")
358
+ return _real_ssl_socket_send(self, *args, **kwargs)
359
+
360
+
361
+ def _guarded_ssl_socket_sendall(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
362
+ if _is_blocking(self):
363
+ _check("ssl.SSLSocket.sendall()")
364
+ return _real_ssl_socket_sendall(self, *args, **kwargs)
365
+
366
+
367
+ def _guarded_ssl_socket_accept(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
368
+ if _is_blocking(self):
369
+ _check("ssl.SSLSocket.accept()")
370
+ return _real_ssl_socket_accept(self, *args, **kwargs)