pynect 0.0.0-stage → 0.2.0
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.
- package/LICENSE +21 -0
- package/README.md +478 -2
- package/SECURITY.md +34 -0
- package/binding.gyp +60 -0
- package/dist/cli/codegen.d.ts +3 -0
- package/dist/cli/codegen.d.ts.map +1 -0
- package/dist/cli/codegen.js +543 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/sync.d.ts +3 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +48 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config/project.d.ts +57 -0
- package/dist/config/project.d.ts.map +1 -0
- package/dist/config/project.js +176 -0
- package/dist/config/project.js.map +1 -0
- package/dist/core/IsolatedWorker.d.ts +39 -0
- package/dist/core/IsolatedWorker.d.ts.map +1 -0
- package/dist/core/IsolatedWorker.js +178 -0
- package/dist/core/IsolatedWorker.js.map +1 -0
- package/dist/core/PyCallable.d.ts +67 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +104 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +239 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +53 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +162 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +75 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +563 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +112 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +546 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +232 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +628 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/ProjectEnvironment.d.ts +63 -0
- package/dist/env/ProjectEnvironment.d.ts.map +1 -0
- package/dist/env/ProjectEnvironment.js +379 -0
- package/dist/env/ProjectEnvironment.js.map +1 -0
- package/dist/env/PyfigAdapter.d.ts +32 -0
- package/dist/env/PyfigAdapter.d.ts.map +1 -0
- package/dist/env/PyfigAdapter.js +10 -0
- package/dist/env/PyfigAdapter.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +48 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +182 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/PythonProvisioner.d.ts +16 -0
- package/dist/env/PythonProvisioner.d.ts.map +1 -0
- package/dist/env/PythonProvisioner.js +42 -0
- package/dist/env/PythonProvisioner.js.map +1 -0
- package/dist/env/VenvManager.d.ts +55 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +153 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/env/executables.d.ts +3 -0
- package/dist/env/executables.d.ts.map +1 -0
- package/dist/env/executables.js +14 -0
- package/dist/env/executables.js.map +1 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/native/NativeAddonLoader.d.ts +11 -0
- package/dist/native/NativeAddonLoader.d.ts.map +1 -0
- package/dist/native/NativeAddonLoader.js +75 -0
- package/dist/native/NativeAddonLoader.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/addon.d.ts +131 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +233 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +60 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Locking.d.ts +3 -0
- package/dist/utils/Locking.d.ts.map +1 -0
- package/dist/utils/Locking.js +10 -0
- package/dist/utils/Locking.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +43 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +114 -4
- package/python/ast_inspector.py +248 -0
- package/python/error_handler.py +432 -0
- package/python/isolated_worker.py +94 -0
- package/python/pynect_dispatcher.py +382 -0
- package/python/pynect_runtime.py +574 -0
- package/python/serializer.py +373 -0
- package/python/type_inspector.py +315 -0
- package/scripts/build-native.js +50 -0
- package/scripts/cli.js +21 -0
- package/scripts/ensure-native.js +85 -0
- package/scripts/postinstall.js +40 -0
- package/scripts/pyfig.js +113 -0
- package/scripts/python-provisioner.js +23 -0
- package/scripts/smoke-python-boundary.js +103 -0
- package/src/native/NativeAddonLoader.ts +98 -0
- package/src/native/gil_guard.cpp +25 -0
- package/src/native/gil_guard.h +174 -0
- package/src/native/pynect_addon.cpp +942 -0
- package/src/native/python_bridge.cpp +904 -0
- package/src/native/python_bridge.h +263 -0
- package/src/native/thread_pool.cpp +375 -0
- package/src/native/thread_pool.h +185 -0
- package/src/native/type_converter.cpp +906 -0
- package/src/native/type_converter.h +271 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""Policy and optional validation for calls crossing the embedded bridge.
|
|
2
|
+
|
|
3
|
+
The module intentionally imports Pydantic lazily. Applications that do not use
|
|
4
|
+
Pydantic keep the normal Python call semantics and do not need it installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import concurrent.futures
|
|
11
|
+
import fnmatch
|
|
12
|
+
import inspect
|
|
13
|
+
import json
|
|
14
|
+
import threading
|
|
15
|
+
from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence
|
|
16
|
+
from typing import Any, get_type_hints
|
|
17
|
+
|
|
18
|
+
_CONFIG: dict[str, Any] = {
|
|
19
|
+
"include": [],
|
|
20
|
+
"exclude": [],
|
|
21
|
+
"typed_only": False,
|
|
22
|
+
"exported_only": False,
|
|
23
|
+
"validation": "auto",
|
|
24
|
+
}
|
|
25
|
+
_LOOP: asyncio.AbstractEventLoop | None = None
|
|
26
|
+
_LOOP_THREAD: threading.Thread | None = None
|
|
27
|
+
_LOOP_READY = threading.Event()
|
|
28
|
+
_LOOP_LOCK = threading.Lock()
|
|
29
|
+
_PENDING: dict[str, concurrent.futures.Future[Any]] = {}
|
|
30
|
+
_CANCELLED: set[str] = set()
|
|
31
|
+
_PENDING_LOCK = threading.Lock()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DispatchPermissionError(PermissionError):
|
|
35
|
+
"""Raised when the configured selection policy rejects a call."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PydanticUnavailableError(RuntimeError):
|
|
39
|
+
"""Raised when validation is required but Pydantic is unavailable."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def export(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
43
|
+
"""Mark a callable for exported-only runtime and codegen policies."""
|
|
44
|
+
|
|
45
|
+
setattr(function, "__pynect_export__", True)
|
|
46
|
+
return function
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def exclude(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
50
|
+
"""Always exclude a callable from runtime calls and generated declarations."""
|
|
51
|
+
|
|
52
|
+
setattr(function, "__pynect_exclude__", True)
|
|
53
|
+
return function
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def configure(options: Mapping[str, Any] | None = None) -> None:
|
|
57
|
+
"""Replace dispatcher policy with normalized, JSON-compatible options."""
|
|
58
|
+
|
|
59
|
+
incoming = dict(options or {})
|
|
60
|
+
validation = incoming.get("validation", "auto")
|
|
61
|
+
if validation not in {"auto", "required", "off"}:
|
|
62
|
+
raise ValueError("dispatcher.validation must be 'auto', 'required', or 'off'")
|
|
63
|
+
_CONFIG.update(
|
|
64
|
+
include=_patterns(incoming.get("include")),
|
|
65
|
+
exclude=_patterns(incoming.get("exclude")),
|
|
66
|
+
typed_only=bool(incoming.get("typedOnly", False)),
|
|
67
|
+
exported_only=bool(incoming.get("exportedOnly", False)),
|
|
68
|
+
validation=validation,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_module_allowed(module_name: str) -> bool:
|
|
73
|
+
"""Return whether importing a module can lead to an allowed callable."""
|
|
74
|
+
|
|
75
|
+
if not _is_public_path(module_name) or _matches_any(module_name, _CONFIG["exclude"]):
|
|
76
|
+
return False
|
|
77
|
+
include: list[str] = _CONFIG["include"]
|
|
78
|
+
if not include:
|
|
79
|
+
return True
|
|
80
|
+
return any(
|
|
81
|
+
fnmatch.fnmatchcase(module_name, pattern)
|
|
82
|
+
or pattern.startswith(f"{module_name}.")
|
|
83
|
+
or pattern.startswith(f"{module_name}:")
|
|
84
|
+
for pattern in include
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def invoke(
|
|
89
|
+
function: Callable[..., Any],
|
|
90
|
+
args: Sequence[Any],
|
|
91
|
+
kwargs: Mapping[str, Any],
|
|
92
|
+
request_id: str = "",
|
|
93
|
+
execution_timeout_ms: int = 0,
|
|
94
|
+
) -> Any:
|
|
95
|
+
"""Authorize, optionally validate, execute, await, and validate one call."""
|
|
96
|
+
|
|
97
|
+
if request_id and _consume_cancelled(request_id):
|
|
98
|
+
raise asyncio.CancelledError(f"Python call was cancelled: {request_id}")
|
|
99
|
+
qualified_name = _qualified_name(function)
|
|
100
|
+
if not _callable_allowed(function, qualified_name):
|
|
101
|
+
raise DispatchPermissionError(f"Python callable is not exported: {qualified_name}")
|
|
102
|
+
|
|
103
|
+
signature = _safe_signature(function)
|
|
104
|
+
hints = _safe_hints(function)
|
|
105
|
+
bound = _bind(signature, args, kwargs) if signature is not None else None
|
|
106
|
+
if bound is not None:
|
|
107
|
+
bound.apply_defaults()
|
|
108
|
+
_validate_arguments(signature, hints, bound.arguments)
|
|
109
|
+
result = function(*bound.args, **bound.kwargs)
|
|
110
|
+
else:
|
|
111
|
+
result = function(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
if inspect.isawaitable(result):
|
|
114
|
+
result = _await(result, request_id, execution_timeout_ms)
|
|
115
|
+
|
|
116
|
+
if request_id and _consume_cancelled(request_id):
|
|
117
|
+
raise asyncio.CancelledError(f"Python call was cancelled: {request_id}")
|
|
118
|
+
|
|
119
|
+
return_annotation = hints.get("return", inspect.Signature.empty)
|
|
120
|
+
if return_annotation not in {inspect.Signature.empty, None, type(None)}:
|
|
121
|
+
result = _validate_value(return_annotation, result, "return value", serialize=True)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _bind(
|
|
126
|
+
signature: inspect.Signature,
|
|
127
|
+
args: Sequence[Any],
|
|
128
|
+
kwargs: Mapping[str, Any],
|
|
129
|
+
) -> inspect.BoundArguments:
|
|
130
|
+
try:
|
|
131
|
+
return signature.bind(*args, **kwargs)
|
|
132
|
+
except TypeError:
|
|
133
|
+
positional = [
|
|
134
|
+
parameter
|
|
135
|
+
for parameter in signature.parameters.values()
|
|
136
|
+
if parameter.name not in {"self", "cls"}
|
|
137
|
+
and parameter.kind
|
|
138
|
+
in {
|
|
139
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
140
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
# The JavaScript proxy historically maps a trailing plain object to
|
|
144
|
+
# kwargs. Recover the common Pydantic-model case where the Python
|
|
145
|
+
# function actually declares one positional object parameter.
|
|
146
|
+
if not args and kwargs and len(positional) == 1 and positional[0].name not in kwargs:
|
|
147
|
+
return signature.bind(dict(kwargs))
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def shutdown() -> None:
|
|
152
|
+
"""Stop the persistent asyncio loop when it was used."""
|
|
153
|
+
|
|
154
|
+
global _LOOP, _LOOP_THREAD
|
|
155
|
+
with _PENDING_LOCK:
|
|
156
|
+
pending = list(_PENDING.values())
|
|
157
|
+
_PENDING.clear()
|
|
158
|
+
_CANCELLED.clear()
|
|
159
|
+
for future in pending:
|
|
160
|
+
future.cancel()
|
|
161
|
+
with _LOOP_LOCK:
|
|
162
|
+
loop = _LOOP
|
|
163
|
+
thread = _LOOP_THREAD
|
|
164
|
+
_LOOP = None
|
|
165
|
+
_LOOP_THREAD = None
|
|
166
|
+
_LOOP_READY.clear()
|
|
167
|
+
if loop is not None:
|
|
168
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
169
|
+
if thread is not None and thread is not threading.current_thread():
|
|
170
|
+
thread.join(timeout=5)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _callable_allowed(function: Callable[..., Any], qualified_name: str) -> bool:
|
|
174
|
+
if getattr(function, "__pynect_exclude__", False):
|
|
175
|
+
return False
|
|
176
|
+
if not _is_public_path(qualified_name):
|
|
177
|
+
return False
|
|
178
|
+
if _matches_any(qualified_name, _CONFIG["exclude"]):
|
|
179
|
+
return False
|
|
180
|
+
include: list[str] = _CONFIG["include"]
|
|
181
|
+
if include and not _matches_any(qualified_name, include):
|
|
182
|
+
return False
|
|
183
|
+
if _CONFIG["exported_only"] and not getattr(function, "__pynect_export__", False):
|
|
184
|
+
return False
|
|
185
|
+
if _CONFIG["typed_only"] and not _fully_typed(function):
|
|
186
|
+
return False
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _fully_typed(function: Callable[..., Any]) -> bool:
|
|
191
|
+
signature = _safe_signature(function)
|
|
192
|
+
if signature is None or signature.return_annotation is inspect.Signature.empty:
|
|
193
|
+
return False
|
|
194
|
+
return all(
|
|
195
|
+
parameter.annotation is not inspect.Parameter.empty
|
|
196
|
+
for parameter in signature.parameters.values()
|
|
197
|
+
if parameter.name not in {"self", "cls"}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _validate_arguments(
|
|
202
|
+
signature: inspect.Signature,
|
|
203
|
+
hints: Mapping[str, Any],
|
|
204
|
+
arguments: MutableMapping[str, Any],
|
|
205
|
+
) -> None:
|
|
206
|
+
for name, value in list(arguments.items()):
|
|
207
|
+
parameter = signature.parameters[name]
|
|
208
|
+
annotation = hints.get(name, parameter.annotation)
|
|
209
|
+
if annotation is inspect.Parameter.empty:
|
|
210
|
+
continue
|
|
211
|
+
if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
212
|
+
arguments[name] = tuple(
|
|
213
|
+
_validate_value(annotation, item, f"argument '{name}'") for item in value
|
|
214
|
+
)
|
|
215
|
+
elif parameter.kind is inspect.Parameter.VAR_KEYWORD:
|
|
216
|
+
arguments[name] = {
|
|
217
|
+
key: _validate_value(annotation, item, f"argument '{name}.{key}'")
|
|
218
|
+
for key, item in value.items()
|
|
219
|
+
}
|
|
220
|
+
else:
|
|
221
|
+
arguments[name] = _validate_value(annotation, value, f"argument '{name}'")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _validate_value(
|
|
225
|
+
annotation: Any,
|
|
226
|
+
value: Any,
|
|
227
|
+
label: str,
|
|
228
|
+
*,
|
|
229
|
+
serialize: bool = False,
|
|
230
|
+
) -> Any:
|
|
231
|
+
mode = _CONFIG["validation"]
|
|
232
|
+
if mode == "off":
|
|
233
|
+
return value
|
|
234
|
+
try:
|
|
235
|
+
import pydantic
|
|
236
|
+
except ImportError:
|
|
237
|
+
if mode == "required":
|
|
238
|
+
raise PydanticUnavailableError(
|
|
239
|
+
f"Pydantic validation is required but unavailable for {label}"
|
|
240
|
+
)
|
|
241
|
+
return value
|
|
242
|
+
|
|
243
|
+
type_adapter = getattr(pydantic, "TypeAdapter", None)
|
|
244
|
+
if type_adapter is not None:
|
|
245
|
+
try:
|
|
246
|
+
adapter = type_adapter(annotation)
|
|
247
|
+
except Exception:
|
|
248
|
+
if mode == "required":
|
|
249
|
+
raise
|
|
250
|
+
return value
|
|
251
|
+
validated = adapter.validate_python(value)
|
|
252
|
+
return adapter.dump_python(validated, mode="json") if serialize else validated
|
|
253
|
+
|
|
254
|
+
parse_obj_as = getattr(pydantic, "parse_obj_as", None)
|
|
255
|
+
if parse_obj_as is None:
|
|
256
|
+
if mode == "required":
|
|
257
|
+
raise PydanticUnavailableError(f"Pydantic does not support {label}")
|
|
258
|
+
return value
|
|
259
|
+
validated = parse_obj_as(annotation, value)
|
|
260
|
+
if not serialize:
|
|
261
|
+
return validated
|
|
262
|
+
encoder = getattr(getattr(pydantic, "json", None), "pydantic_encoder", None)
|
|
263
|
+
return json.loads(json.dumps(validated, default=encoder)) if encoder else validated
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def cancel(request_id: str) -> bool:
|
|
267
|
+
"""Cancel a queued-to-loop coroutine or mark a not-yet-registered call."""
|
|
268
|
+
|
|
269
|
+
if not request_id:
|
|
270
|
+
return False
|
|
271
|
+
with _PENDING_LOCK:
|
|
272
|
+
future = _PENDING.get(request_id)
|
|
273
|
+
if future is None:
|
|
274
|
+
_CANCELLED.add(request_id)
|
|
275
|
+
return True
|
|
276
|
+
return future.cancel()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _consume_cancelled(request_id: str) -> bool:
|
|
280
|
+
with _PENDING_LOCK:
|
|
281
|
+
if request_id not in _CANCELLED:
|
|
282
|
+
return False
|
|
283
|
+
_CANCELLED.remove(request_id)
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _await(value: Awaitable[Any], request_id: str, timeout_ms: int) -> Any:
|
|
288
|
+
loop = _ensure_loop()
|
|
289
|
+
|
|
290
|
+
async def resolve() -> Any:
|
|
291
|
+
return await value
|
|
292
|
+
|
|
293
|
+
future = asyncio.run_coroutine_threadsafe(resolve(), loop)
|
|
294
|
+
if request_id:
|
|
295
|
+
with _PENDING_LOCK:
|
|
296
|
+
if request_id in _CANCELLED:
|
|
297
|
+
_CANCELLED.remove(request_id)
|
|
298
|
+
future.cancel()
|
|
299
|
+
else:
|
|
300
|
+
_PENDING[request_id] = future
|
|
301
|
+
try:
|
|
302
|
+
timeout = timeout_ms / 1000 if timeout_ms > 0 else None
|
|
303
|
+
return future.result(timeout=timeout)
|
|
304
|
+
except concurrent.futures.TimeoutError as error:
|
|
305
|
+
future.cancel()
|
|
306
|
+
raise TimeoutError(
|
|
307
|
+
f"Python async call exceeded execution timeout of {timeout_ms}ms"
|
|
308
|
+
) from error
|
|
309
|
+
except concurrent.futures.CancelledError as error:
|
|
310
|
+
raise asyncio.CancelledError(
|
|
311
|
+
f"Python async call was cancelled: {request_id or '<unknown>'}"
|
|
312
|
+
) from error
|
|
313
|
+
finally:
|
|
314
|
+
if request_id:
|
|
315
|
+
with _PENDING_LOCK:
|
|
316
|
+
_PENDING.pop(request_id, None)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _ensure_loop() -> asyncio.AbstractEventLoop:
|
|
320
|
+
global _LOOP, _LOOP_THREAD
|
|
321
|
+
with _LOOP_LOCK:
|
|
322
|
+
if _LOOP is not None and _LOOP.is_running():
|
|
323
|
+
return _LOOP
|
|
324
|
+
loop = asyncio.new_event_loop()
|
|
325
|
+
_LOOP = loop
|
|
326
|
+
|
|
327
|
+
def run() -> None:
|
|
328
|
+
asyncio.set_event_loop(loop)
|
|
329
|
+
_LOOP_READY.set()
|
|
330
|
+
loop.run_forever()
|
|
331
|
+
loop.close()
|
|
332
|
+
|
|
333
|
+
_LOOP_THREAD = threading.Thread(
|
|
334
|
+
target=run,
|
|
335
|
+
name="pynect-asyncio",
|
|
336
|
+
daemon=True,
|
|
337
|
+
)
|
|
338
|
+
_LOOP_THREAD.start()
|
|
339
|
+
_LOOP_READY.wait(timeout=5)
|
|
340
|
+
if _LOOP is None or not _LOOP.is_running():
|
|
341
|
+
raise RuntimeError("Could not start the Pynect asyncio loop")
|
|
342
|
+
return _LOOP
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _qualified_name(function: Callable[..., Any]) -> str:
|
|
346
|
+
module = getattr(function, "__module__", "")
|
|
347
|
+
name = getattr(function, "__qualname__", getattr(function, "__name__", "<callable>"))
|
|
348
|
+
return f"{module}.{name}" if module else name
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _safe_signature(function: Callable[..., Any]) -> inspect.Signature | None:
|
|
352
|
+
try:
|
|
353
|
+
return inspect.signature(function)
|
|
354
|
+
except (TypeError, ValueError):
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _safe_hints(function: Callable[..., Any]) -> dict[str, Any]:
|
|
359
|
+
try:
|
|
360
|
+
return get_type_hints(function, include_extras=True)
|
|
361
|
+
except Exception:
|
|
362
|
+
return dict(getattr(function, "__annotations__", {}))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _patterns(value: Any) -> list[str]:
|
|
366
|
+
if value is None:
|
|
367
|
+
return []
|
|
368
|
+
if isinstance(value, str):
|
|
369
|
+
return [value]
|
|
370
|
+
return [str(item) for item in value]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _matches_any(value: str, patterns: Sequence[str]) -> bool:
|
|
374
|
+
leaf = value.rsplit(".", 1)[-1]
|
|
375
|
+
return any(
|
|
376
|
+
fnmatch.fnmatchcase(value, pattern) or fnmatch.fnmatchcase(leaf, pattern)
|
|
377
|
+
for pattern in patterns
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _is_public_path(value: str) -> bool:
|
|
382
|
+
return all(not part.startswith("_") for part in value.replace(":", ".").split("."))
|