isolate 0.22.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.
Files changed (61) hide show
  1. isolate/__init__.py +3 -0
  2. isolate/_isolate_version.py +34 -0
  3. isolate/_version.py +6 -0
  4. isolate/backends/__init__.py +2 -0
  5. isolate/backends/_base.py +132 -0
  6. isolate/backends/common.py +259 -0
  7. isolate/backends/conda.py +215 -0
  8. isolate/backends/container.py +64 -0
  9. isolate/backends/local.py +46 -0
  10. isolate/backends/pyenv.py +143 -0
  11. isolate/backends/remote.py +141 -0
  12. isolate/backends/settings.py +121 -0
  13. isolate/backends/virtualenv.py +204 -0
  14. isolate/common/__init__.py +0 -0
  15. isolate/common/timestamp.py +15 -0
  16. isolate/connections/__init__.py +21 -0
  17. isolate/connections/_local/__init__.py +2 -0
  18. isolate/connections/_local/_base.py +190 -0
  19. isolate/connections/_local/agent_startup.py +53 -0
  20. isolate/connections/common.py +121 -0
  21. isolate/connections/grpc/__init__.py +1 -0
  22. isolate/connections/grpc/_base.py +175 -0
  23. isolate/connections/grpc/agent.py +284 -0
  24. isolate/connections/grpc/configuration.py +23 -0
  25. isolate/connections/grpc/definitions/__init__.py +11 -0
  26. isolate/connections/grpc/definitions/agent.proto +18 -0
  27. isolate/connections/grpc/definitions/agent_pb2.py +29 -0
  28. isolate/connections/grpc/definitions/agent_pb2.pyi +44 -0
  29. isolate/connections/grpc/definitions/agent_pb2_grpc.py +68 -0
  30. isolate/connections/grpc/definitions/common.proto +49 -0
  31. isolate/connections/grpc/definitions/common_pb2.py +35 -0
  32. isolate/connections/grpc/definitions/common_pb2.pyi +152 -0
  33. isolate/connections/grpc/definitions/common_pb2_grpc.py +4 -0
  34. isolate/connections/grpc/interface.py +71 -0
  35. isolate/connections/ipc/__init__.py +5 -0
  36. isolate/connections/ipc/_base.py +225 -0
  37. isolate/connections/ipc/agent.py +205 -0
  38. isolate/logger.py +53 -0
  39. isolate/logs.py +76 -0
  40. isolate/py.typed +0 -0
  41. isolate/registry.py +53 -0
  42. isolate/server/__init__.py +1 -0
  43. isolate/server/definitions/__init__.py +13 -0
  44. isolate/server/definitions/server.proto +80 -0
  45. isolate/server/definitions/server_pb2.py +56 -0
  46. isolate/server/definitions/server_pb2.pyi +241 -0
  47. isolate/server/definitions/server_pb2_grpc.py +205 -0
  48. isolate/server/health/__init__.py +11 -0
  49. isolate/server/health/health.proto +23 -0
  50. isolate/server/health/health_pb2.py +32 -0
  51. isolate/server/health/health_pb2.pyi +66 -0
  52. isolate/server/health/health_pb2_grpc.py +99 -0
  53. isolate/server/health_server.py +40 -0
  54. isolate/server/interface.py +27 -0
  55. isolate/server/server.py +735 -0
  56. isolate-0.22.0.dist-info/METADATA +88 -0
  57. isolate-0.22.0.dist-info/RECORD +61 -0
  58. isolate-0.22.0.dist-info/WHEEL +5 -0
  59. isolate-0.22.0.dist-info/entry_points.txt +7 -0
  60. isolate-0.22.0.dist-info/licenses/LICENSE +201 -0
  61. isolate-0.22.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ from dataclasses import dataclass, field
7
+ from functools import partial
8
+ from pathlib import Path
9
+ from typing import Any, ClassVar
10
+
11
+ from isolate.backends import BaseEnvironment, EnvironmentCreationError
12
+ from isolate.backends.common import (
13
+ active_python,
14
+ get_executable,
15
+ get_executable_path,
16
+ logged_io,
17
+ optional_import,
18
+ sha256_digest_of,
19
+ )
20
+ from isolate.backends.settings import DEFAULT_SETTINGS, IsolateSettings
21
+ from isolate.connections import PythonIPC
22
+ from isolate.logs import LogLevel
23
+
24
+ _UV_RESOLVER_EXECUTABLE = os.environ.get("ISOLATE_UV_EXE", "uv")
25
+ _UV_RESOLVER_HOME = os.getenv("ISOLATE_UV_HOME")
26
+
27
+
28
+ @dataclass
29
+ class VirtualPythonEnvironment(BaseEnvironment[Path]):
30
+ BACKEND_NAME: ClassVar[str] = "virtualenv"
31
+
32
+ requirements: list[str] = field(default_factory=list)
33
+ constraints_file: os.PathLike | None = None
34
+ python_version: str | None = None
35
+ extra_index_urls: list[str] = field(default_factory=list)
36
+ tags: list[str] = field(default_factory=list)
37
+ resolver: str | None = None
38
+
39
+ @classmethod
40
+ def from_config(
41
+ cls,
42
+ config: dict[str, Any],
43
+ settings: IsolateSettings = DEFAULT_SETTINGS,
44
+ ) -> BaseEnvironment:
45
+ environment = cls(**config)
46
+ environment.apply_settings(settings)
47
+ if environment.resolver not in ("uv", None):
48
+ raise ValueError(
49
+ "Only 'uv' is supported as a resolver for virtualenv environments."
50
+ )
51
+ return environment
52
+
53
+ @property
54
+ def key(self) -> str:
55
+ if self.constraints_file is not None:
56
+ with open(self.constraints_file) as stream:
57
+ constraints = stream.read().splitlines()
58
+ else:
59
+ constraints = []
60
+
61
+ extras = []
62
+ if self.resolver is not None:
63
+ extras.append(f"resolver={self.resolver}")
64
+
65
+ active_python_version = self.python_version or active_python()
66
+ return sha256_digest_of(
67
+ active_python_version,
68
+ *self.requirements,
69
+ *constraints,
70
+ *self.extra_index_urls,
71
+ *sorted(self.tags),
72
+ # This is backwards compatible with environments not using
73
+ # the 'resolver' field.
74
+ *extras,
75
+ )
76
+
77
+ def install_requirements(self, path: Path) -> None:
78
+ """Install the requirements of this environment using 'pip' to the
79
+ given virtualenv path.
80
+
81
+ If there are any constraint files specified, they will be also passed to
82
+ the package resolver.
83
+ """
84
+ if not self.requirements:
85
+ return None
86
+
87
+ self.log(f"Installing requirements: {', '.join(self.requirements)}")
88
+ environ = os.environ.copy()
89
+
90
+ if self.resolver == "uv":
91
+ # Set VIRTUAL_ENV to the actual path of the environment since that is
92
+ # how uv discovers the environment. This is necessary when using uv
93
+ # as the resolver.
94
+ environ["VIRTUAL_ENV"] = str(path)
95
+ base_pip_cmd = [
96
+ get_executable(_UV_RESOLVER_EXECUTABLE, _UV_RESOLVER_HOME),
97
+ "pip",
98
+ ]
99
+ else:
100
+ base_pip_cmd = [get_executable_path(path, "pip")]
101
+
102
+ pip_cmd: list[str | os.PathLike] = [
103
+ *base_pip_cmd, # type: ignore
104
+ "install",
105
+ *self.requirements,
106
+ ]
107
+ if self.constraints_file:
108
+ pip_cmd.extend(["-c", self.constraints_file])
109
+
110
+ for extra_index_url in self.extra_index_urls:
111
+ pip_cmd.extend(["--extra-index-url", extra_index_url])
112
+
113
+ with logged_io(partial(self.log, level=LogLevel.INFO)) as (stdout, stderr, _):
114
+ try:
115
+ subprocess.check_call(
116
+ pip_cmd,
117
+ stdout=stdout,
118
+ stderr=stderr,
119
+ env=environ,
120
+ )
121
+ except subprocess.SubprocessError as exc:
122
+ raise EnvironmentCreationError(f"Failure during 'pip install': {exc}")
123
+
124
+ def _install_python_through_pyenv(self) -> str:
125
+ from isolate.backends.pyenv import PyenvEnvironment
126
+
127
+ self.log(
128
+ f"Requested Python version of {self.python_version} is not available "
129
+ "in the system, attempting to install it through pyenv."
130
+ )
131
+
132
+ pyenv = PyenvEnvironment.from_config(
133
+ {"python_version": self.python_version},
134
+ settings=self.settings,
135
+ )
136
+ return str(get_executable_path(pyenv.create(), "python"))
137
+
138
+ def _decide_python(self) -> str:
139
+ from isolate.backends.pyenv import _get_pyenv_executable
140
+
141
+ builtin_discovery = optional_import("virtualenv.discovery.builtin")
142
+ interpreter = builtin_discovery.get_interpreter(self.python_version, ())
143
+ if interpreter is not None:
144
+ return interpreter.executable
145
+
146
+ try:
147
+ _get_pyenv_executable()
148
+ except Exception:
149
+ raise EnvironmentCreationError(
150
+ f"Python {self.python_version} is not available in your "
151
+ "system. Please install it first."
152
+ ) from None
153
+ else:
154
+ return self._install_python_through_pyenv()
155
+
156
+ def create(self, *, force: bool = False) -> Path:
157
+ virtualenv = optional_import("virtualenv")
158
+
159
+ venv_path = self.settings.cache_dir_for(self)
160
+ completion_marker = self.settings.completion_marker_for(venv_path)
161
+ with self.settings.cache_lock_for(venv_path):
162
+ if not force:
163
+ is_cached = venv_path.exists()
164
+ if self.settings.strict_cache:
165
+ is_cached &= completion_marker.exists()
166
+
167
+ if is_cached:
168
+ return venv_path
169
+
170
+ self.log(f"Creating the environment at '{venv_path}'")
171
+
172
+ args = [str(venv_path)]
173
+ if self.python_version:
174
+ args.append(f"--python={self._decide_python()}")
175
+
176
+ try:
177
+ # This is not an official API, so it can throw anything at us.
178
+ virtualenv.cli_run(args)
179
+ except (SystemExit, RuntimeError, OSError) as exc:
180
+ raise EnvironmentCreationError(
181
+ f"Failed to create the environment at '{venv_path}': {exc}"
182
+ )
183
+
184
+ self.install_requirements(venv_path)
185
+ completion_marker.touch()
186
+
187
+ self.log(f"New environment cached at '{venv_path}'")
188
+ return venv_path
189
+
190
+ def destroy(self, connection_key: Path) -> None:
191
+ with self.settings.cache_lock_for(connection_key):
192
+ # It might be destroyed already (when we are awaiting
193
+ # for the lock to be released).
194
+ if not connection_key.exists():
195
+ return
196
+
197
+ shutil.rmtree(connection_key)
198
+
199
+ def exists(self) -> bool:
200
+ path = self.settings.cache_dir_for(self)
201
+ return path.exists()
202
+
203
+ def open_connection(self, connection_key: Path) -> PythonIPC:
204
+ return PythonIPC(self, connection_key)
File without changes
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from google.protobuf.timestamp_pb2 import Timestamp
6
+
7
+
8
+ def from_datetime(time: datetime) -> Timestamp:
9
+ timestamp = Timestamp()
10
+ timestamp.FromDatetime(time)
11
+ return timestamp
12
+
13
+
14
+ def to_datetime(timestamp: Timestamp) -> datetime:
15
+ return timestamp.ToDatetime(tzinfo=timezone.utc)
@@ -0,0 +1,21 @@
1
+ import importlib
2
+
3
+ from isolate.connections.ipc import IsolatedProcessConnection, PythonIPC # noqa: F401
4
+
5
+
6
+ def __getattr__(name):
7
+ if name == "LocalPythonGRPC":
8
+ extra = "grpc"
9
+ module_name = "isolate.connections.grpc"
10
+ else:
11
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
12
+
13
+ try:
14
+ module = importlib.import_module(module_name)
15
+ except ImportError:
16
+ raise AttributeError(
17
+ f"For using {name!r} you need to install isolate with {extra!r} support."
18
+ f'\n $ pip install "isolate[{extra}]"'
19
+ )
20
+
21
+ return getattr(module, name)
@@ -0,0 +1,2 @@
1
+ from isolate.connections._local import agent_startup # noqa: F401
2
+ from isolate.connections._local._base import PythonExecutionBase # noqa: F401
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ import sysconfig
6
+ from contextlib import contextmanager
7
+ from dataclasses import dataclass, field
8
+ from functools import partial
9
+ from pathlib import Path
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ Any,
13
+ Generic,
14
+ Iterator,
15
+ TypeVar,
16
+ )
17
+
18
+ from isolate import __version__ as isolate_version
19
+ from isolate.backends.common import active_python, get_executable_path, logged_io
20
+ from isolate.connections.common import AGENT_SIGNATURE
21
+ from isolate.logs import LogLevel, LogSource
22
+
23
+ if TYPE_CHECKING:
24
+ from isolate.backends import BaseEnvironment
25
+
26
+ ConnectionType = TypeVar("ConnectionType")
27
+
28
+
29
+ def binary_path_for(*search_paths: Path) -> str:
30
+ """Return the binary search path for the given 'search_paths'.
31
+ It will be a combination of the 'bin/' folders in them and
32
+ the existing PATH environment variable."""
33
+
34
+ paths = []
35
+ for search_path in search_paths:
36
+ path = sysconfig.get_path("scripts", vars={"base": search_path})
37
+ paths.append(path)
38
+ # Some distributions (conda) might include both a 'bin' and
39
+ # a 'scripts' folder.
40
+
41
+ auxilary_binary_path = search_path / "bin"
42
+ if path != auxilary_binary_path and auxilary_binary_path.exists():
43
+ paths.append(str(auxilary_binary_path))
44
+
45
+ if "PATH" in os.environ:
46
+ paths.append(os.environ["PATH"])
47
+
48
+ return os.pathsep.join(paths)
49
+
50
+
51
+ def python_path_for(*search_paths: Path) -> str:
52
+ """Return the PYTHONPATH for the library paths residing
53
+ in the given 'search_paths'. The order of the paths is
54
+ preserved."""
55
+ assert len(search_paths) >= 1
56
+ lib_paths = []
57
+ for search_path in search_paths:
58
+ # sysconfig defines the schema of the directories under
59
+ # any comforming Python installation (like venv, conda, etc.).
60
+ #
61
+ # Be aware that Debian's system installation does not
62
+ # comform sysconfig.
63
+ raw_glob_expr = sysconfig.get_path(
64
+ "purelib",
65
+ vars={
66
+ "base": search_path,
67
+ "python_version": "*",
68
+ "py_version_short": "*",
69
+ "py_version_nodot": "*",
70
+ },
71
+ )
72
+ relative_glob_expr = Path(raw_glob_expr).relative_to(search_path).as_posix()
73
+
74
+ # Try to find expand the Python version in the path. This is
75
+ # necessary for supporting multiple Python versions in the same
76
+ # environment.
77
+ for file in search_path.glob(relative_glob_expr):
78
+ lib_paths.append(str(file))
79
+
80
+ return os.pathsep.join(lib_paths)
81
+
82
+
83
+ @dataclass
84
+ class PythonExecutionBase(Generic[ConnectionType]):
85
+ """A generic Python execution implementation that can trigger a new process
86
+ and start watching stdout/stderr for the logs. The environment_path must be
87
+ the base directory of a new Python environment (structure that complies with
88
+ sysconfig). Python binary inside that environment will be used to run the
89
+ agent process.
90
+
91
+ If set, extra_inheritance_paths allows extending the custom package search
92
+ system with additional environments. As an example, the current environment_path
93
+ might point to an environment with numpy and the extra_inheritance_paths might
94
+ point to an environment with pandas. In this case, the agent process will have
95
+ access to both numpy and pandas. The order is important here, as the first
96
+ path in the list will be the first one to be looked up (so if there is multiple
97
+ versions of the same package in different environments, the one in the first
98
+ path will take precedence). Dependency resolution and compatibility must be
99
+ handled by the user."""
100
+
101
+ environment: BaseEnvironment
102
+ environment_path: Path
103
+ extra_inheritance_paths: list[Path] = field(default_factory=list)
104
+
105
+ @contextmanager
106
+ def start_process(
107
+ self,
108
+ connection: ConnectionType,
109
+ *args: Any,
110
+ **kwargs: Any,
111
+ ) -> Iterator[subprocess.Popen]:
112
+ """Start the agent process with the Python binary available inside the
113
+ bound environment."""
114
+
115
+ python_version = getattr(self.environment, "python_version", active_python())
116
+ try:
117
+ # prefer the specific version if available.
118
+ python_executable = get_executable_path(
119
+ self.environment_path, f"python{python_version}"
120
+ )
121
+ except FileNotFoundError:
122
+ # fallback to the generic binary.
123
+ python_executable = get_executable_path(self.environment_path, "python")
124
+
125
+ with logged_io(
126
+ partial(
127
+ self.handle_agent_log, source=LogSource.USER, level=LogLevel.STDOUT
128
+ ),
129
+ partial(
130
+ self.handle_agent_log, source=LogSource.USER, level=LogLevel.STDERR
131
+ ),
132
+ partial(
133
+ self.handle_agent_log, source=LogSource.BRIDGE, level=LogLevel.TRACE
134
+ ),
135
+ ) as (stdout, stderr, log_fd):
136
+ yield subprocess.Popen(
137
+ self.get_python_cmd(python_executable, connection, log_fd),
138
+ env=self.get_env_vars(),
139
+ stdout=stdout,
140
+ stderr=stderr,
141
+ pass_fds=(log_fd,),
142
+ text=True,
143
+ )
144
+
145
+ def get_env_vars(self) -> dict[str, str]:
146
+ """Return the environment variables to run the agent process with. By default
147
+ PYTHONUNBUFFERED is set to 1 to ensure the prints to stdout/stderr are reflect
148
+ immediately (so that we can seamlessly transfer logs)."""
149
+
150
+ custom_vars = {}
151
+ custom_vars["ISOLATE_SERVER_VERSION"] = isolate_version
152
+ custom_vars[AGENT_SIGNATURE] = "1"
153
+ custom_vars["PYTHONUNBUFFERED"] = "1"
154
+
155
+ # NOTE: we don't have to manually set PYTHONPATH here if we are
156
+ # using a single environment since python will automatically
157
+ # use the proper path.
158
+ if self.extra_inheritance_paths:
159
+ # The order here should reflect the order of the inheritance
160
+ # where the actual environment already takes precedence.
161
+ custom_vars["PYTHONPATH"] = python_path_for(
162
+ self.environment_path, *self.extra_inheritance_paths
163
+ )
164
+
165
+ # But the PATH must be always set since it will be not be
166
+ # automatically set by Python (think of this as ./venv/bin/activate)
167
+ custom_vars["PATH"] = binary_path_for(
168
+ self.environment_path, *self.extra_inheritance_paths
169
+ )
170
+
171
+ return {
172
+ **os.environ,
173
+ **custom_vars,
174
+ }
175
+
176
+ def get_python_cmd(
177
+ self,
178
+ executable: Path,
179
+ connection: ConnectionType,
180
+ log_fd: int,
181
+ ) -> list[str | Path]:
182
+ """Return the command to run the agent process with."""
183
+ raise NotImplementedError
184
+
185
+ def handle_agent_log(
186
+ self, line: str, *, level: LogLevel, source: LogSource
187
+ ) -> None:
188
+ """Handle a log line emitted by the agent process. The level will be either
189
+ STDOUT or STDERR."""
190
+ raise NotImplementedError
@@ -0,0 +1,53 @@
1
+ """
2
+ Agent process execution wrapper for handling extended PYTHONPATH.
3
+ """
4
+
5
+ import os
6
+ import runpy
7
+ import site
8
+ import sys
9
+ import traceback
10
+
11
+
12
+ def load_pth_files() -> None:
13
+ """Each site dir in Python can contain some .pth files, which are
14
+ basically instructions that tell Python to load other stuff. This is
15
+ generally used for editable installations, and just setting PYTHONPATH
16
+ won't make them expand so we need manually process them. Luckily, site
17
+ module can simply take the list of new paths and recognize them.
18
+
19
+ https://docs.python.org/3/tutorial/modules.html#the-module-search-path
20
+ """
21
+ python_path = os.getenv("PYTHONPATH")
22
+ if python_path is None:
23
+ return None
24
+
25
+ # TODO: The order here is the same as the one that is used for generating the
26
+ # PYTHONPATH. The only problem that might occur is that, on a chain with
27
+ # 3 ore more nodes (A, B, C), if X is installed as an editable package to
28
+ # B and a normal package to C, then C might actually take precedence. This
29
+ # will need to be fixed once we are dealing with more than 2 nodes and editable
30
+ # packages.
31
+ for site_dir in python_path.split(os.pathsep):
32
+ try:
33
+ site.addsitedir(site_dir)
34
+ except Exception:
35
+ # NOTE: there could be .pth files that are model weights and not
36
+ # python path configuration files.
37
+ traceback.print_exc()
38
+ print(f"Error adding site directory {site_dir}, skipping...")
39
+
40
+
41
+ def main():
42
+ real_agent, *real_arguments = sys.argv[1:]
43
+
44
+ load_pth_files()
45
+ # TODO(feat): implement a check to parse "agent-requires" line and see if
46
+ # all the dependencies are installed.
47
+ sys.argv = [real_agent] + real_arguments
48
+ runpy.run_path(real_agent, run_name="__main__")
49
+
50
+
51
+ if __name__ == "__main__":
52
+ load_pth_files()
53
+ main()
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import os
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any, Iterator, cast
8
+
9
+ from tblib import Traceback, TracebackParseError
10
+ from tblib.pickling_support import install as tblib_install
11
+
12
+ if TYPE_CHECKING:
13
+ from typing import Protocol
14
+
15
+ class SerializationBackend(Protocol):
16
+ def loads(self, data: bytes) -> Any: ...
17
+
18
+ def dumps(self, obj: Any) -> bytes: ...
19
+
20
+
21
+ AGENT_SIGNATURE = "IS_ISOLATE_AGENT"
22
+
23
+
24
+ @dataclass
25
+ class SerializationError(Exception):
26
+ """An error that happened during the serialization process."""
27
+
28
+ message: str
29
+
30
+
31
+ # NOTE: tblib's install() will search for BaseException subclasses,
32
+ # so we have to call it after the SerializationError is defined.
33
+ tblib_install()
34
+
35
+
36
+ @contextmanager
37
+ def _step(message: str) -> Iterator[None]:
38
+ """A context manager to capture every expression
39
+ underneath it and if any of them fails for any reason
40
+ then it will raise a SerializationError with the
41
+ given message."""
42
+
43
+ try:
44
+ yield
45
+ except BaseException as exception:
46
+ raise SerializationError("Error while " + message) from exception
47
+
48
+
49
+ def as_serialization_method(backend: Any) -> SerializationBackend:
50
+ """Ensures that the given backend has loads/dumps methods, and returns
51
+ it as is (also convinces type checkers that the given object satisfies
52
+ the serialization protocol)."""
53
+
54
+ if not hasattr(backend, "loads") or not hasattr(backend, "dumps"):
55
+ raise TypeError(
56
+ f"The given serialization backend ({backend.__name__}) does "
57
+ "not have one of the required methods (loads/dumps)."
58
+ )
59
+
60
+ return cast("SerializationBackend", backend)
61
+
62
+
63
+ def load_serialized_object(
64
+ serialization_method: str,
65
+ raw_object: bytes,
66
+ *,
67
+ was_it_raised: bool = False,
68
+ stringized_traceback: str | None = None,
69
+ ) -> Any:
70
+ """Load the given serialized object using the given serialization method. If
71
+ anything fails, then a SerializationError will be raised. If the was_it_raised
72
+ flag is set to true, then the given object will be raised as an exception (instead
73
+ of being returned)."""
74
+
75
+ with _step(f"preparing the serialization backend ({serialization_method})"):
76
+ serialization_backend = as_serialization_method(
77
+ importlib.import_module(serialization_method)
78
+ )
79
+
80
+ with _step("deserializing the given object"):
81
+ result = serialization_backend.loads(raw_object)
82
+
83
+ if was_it_raised:
84
+ raise prepare_exc(result, stringized_traceback=stringized_traceback)
85
+ else:
86
+ return result
87
+
88
+
89
+ def serialize_object(serialization_method: str, object: Any) -> bytes:
90
+ """Serialize the given object using the given serialization method. If
91
+ anything fails, then a SerializationError will be raised."""
92
+
93
+ with _step(f"preparing the serialization backend ({serialization_method})"):
94
+ serialization_backend = as_serialization_method(
95
+ importlib.import_module(serialization_method)
96
+ )
97
+
98
+ with _step("serializing the given object"):
99
+ return serialization_backend.dumps(object)
100
+
101
+
102
+ def is_agent() -> bool:
103
+ """Returns true if the current process is an isolate agent."""
104
+ return os.environ.get(AGENT_SIGNATURE) == "1"
105
+
106
+
107
+ def prepare_exc(
108
+ exc: BaseException,
109
+ *,
110
+ stringized_traceback: str | None = None,
111
+ ) -> BaseException:
112
+ if stringized_traceback:
113
+ try:
114
+ traceback = Traceback.from_string(stringized_traceback).as_traceback()
115
+ except TracebackParseError:
116
+ traceback = None
117
+ else:
118
+ traceback = None
119
+
120
+ exc.__traceback__ = traceback
121
+ return exc
@@ -0,0 +1 @@
1
+ from isolate.connections.grpc._base import AgentError, LocalPythonGRPC # noqa: F401