splime 0.1.2__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.
- spl/__init__.py +14 -0
- spl/client.py +1364 -0
- spl/core/__init__.py +23 -0
- spl/core/common.py +350 -0
- spl/core/entities/__init__.py +0 -0
- spl/core/entities/adapter.py +210 -0
- spl/core/entities/artifact.py +141 -0
- spl/core/entities/control.py +45 -0
- spl/core/entities/distribution.py +65 -0
- spl/core/entities/function.py +254 -0
- spl/core/entities/local_function.py +286 -0
- spl/core/entities/misc.py +14 -0
- spl/core/entities/module.py +88 -0
- spl/core/entities/node.py +286 -0
- spl/core/entities/node_function.py +79 -0
- spl/core/entities/node_remote.py +295 -0
- spl/core/entities/pipeline.py +436 -0
- spl/core/entities/scalar.py +55 -0
- spl/core/ir/__init__.py +0 -0
- spl/core/ir/common.py +34 -0
- spl/core/ir/parse.py +79 -0
- spl/core/ir/unparse.py +29 -0
- spl/core/ir/utils.py +163 -0
- spl/daemon/__init__.py +23 -0
- spl/daemon/__main__.py +11 -0
- spl/daemon/cli.py +582 -0
- spl/daemon/client.py +43 -0
- spl/daemon/docker_environment.py +329 -0
- spl/daemon/docker_pool.py +516 -0
- spl/daemon/environment.py +228 -0
- spl/daemon/environment_base.py +479 -0
- spl/daemon/heartbeat_service.py +119 -0
- spl/daemon/metadata.py +427 -0
- spl/daemon/remote_client.py +457 -0
- spl/daemon/repositories/__init__.py +17 -0
- spl/daemon/repositories/env.py +323 -0
- spl/daemon/repositories/library.py +181 -0
- spl/daemon/repositories/object.py +997 -0
- spl/daemon/repositories/run.py +279 -0
- spl/daemon/repositories/server_connection.py +657 -0
- spl/daemon/repositories/sync_event.py +129 -0
- spl/daemon/routes/__init__.py +1 -0
- spl/daemon/routes/_helpers.py +147 -0
- spl/daemon/routes/artifacts.py +77 -0
- spl/daemon/routes/diagnostics.py +114 -0
- spl/daemon/routes/envs.py +82 -0
- spl/daemon/routes/libraries.py +129 -0
- spl/daemon/routes/objects.py +174 -0
- spl/daemon/routes/remote.py +56 -0
- spl/daemon/routes/runs.py +96 -0
- spl/daemon/routes/server_connections.py +86 -0
- spl/daemon/runtime_backend.py +368 -0
- spl/daemon/runtime_config.py +133 -0
- spl/daemon/runtime_dependencies.py +459 -0
- spl/daemon/secret_store.py +187 -0
- spl/daemon/server.py +2224 -0
- spl/daemon/server_connection.py +267 -0
- spl/daemon/services/__init__.py +1 -0
- spl/daemon/services/sync.py +76 -0
- spl/daemon/signature.py +376 -0
- spl/daemon/storage_base.py +542 -0
- spl/daemon/store.py +436 -0
- spl/daemon/worker.py +526 -0
- spl/daemon_client.py +945 -0
- spl/pipeline_widget.py +1452 -0
- spl/py.typed +0 -0
- spl/server_client.py +787 -0
- splime-0.1.2.dist-info/METADATA +189 -0
- splime-0.1.2.dist-info/RECORD +74 -0
- splime-0.1.2.dist-info/WHEEL +5 -0
- splime-0.1.2.dist-info/entry_points.txt +2 -0
- splime-0.1.2.dist-info/licenses/LICENSE +201 -0
- splime-0.1.2.dist-info/licenses/NOTICE +8 -0
- splime-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Inline first-party (local) helper functions into the serialized entity.
|
|
2
|
+
|
|
3
|
+
When a wrapped function depends on other functions that the user defined in
|
|
4
|
+
their own local files -- plain modules on disk that are **not** installed via
|
|
5
|
+
pip and are **not** part of the standard library -- a bare ``from my_module
|
|
6
|
+
import helper`` cannot be reproduced on another machine or inside the daemon:
|
|
7
|
+
the local module simply is not importable there.
|
|
8
|
+
|
|
9
|
+
This module teaches :data:`spl.core.ir.parse.ir_parse` to treat such helpers
|
|
10
|
+
exactly like ``__main__`` functions: it inlines their source as a
|
|
11
|
+
:class:`~spl.core.entities.function.DFunction` and recurses into *their* own
|
|
12
|
+
dependencies, following the import graph across as many local files as needed
|
|
13
|
+
(``top`` -> ``helpers`` -> ``deeper`` -> ...). Because every local helper is
|
|
14
|
+
inlined, no ``from local_module import ...`` statement is ever emitted into the
|
|
15
|
+
serialized output -- the local import is excluded from the "include" section by
|
|
16
|
+
construction.
|
|
17
|
+
|
|
18
|
+
Third-party (pip) objects and the standard library keep the existing behaviour:
|
|
19
|
+
they are referenced through imports and recorded as distributions, never
|
|
20
|
+
inlined.
|
|
21
|
+
|
|
22
|
+
Dispatch priority
|
|
23
|
+
-----------------
|
|
24
|
+
The handler registered here intentionally takes precedence over the generic
|
|
25
|
+
``from module import name`` handler in :mod:`spl.core.entities.module`. Priority
|
|
26
|
+
is established purely through import order in ``spl/core/__init__.py`` (this
|
|
27
|
+
module is imported before ``spl.core.entities.module``), so no existing file
|
|
28
|
+
needs its dispatch predicate changed. ``is_inlinable_local_function`` is also
|
|
29
|
+
written to be strictly narrower than the generic predicate and to never raise,
|
|
30
|
+
so even if the order were lost the worst case is the previous behaviour, not a
|
|
31
|
+
crash.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import ast
|
|
37
|
+
import builtins
|
|
38
|
+
import inspect
|
|
39
|
+
import sys
|
|
40
|
+
import sysconfig
|
|
41
|
+
import textwrap
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from functools import lru_cache
|
|
44
|
+
from itertools import chain
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from types import FunctionType
|
|
47
|
+
from typing import Any, Generator
|
|
48
|
+
|
|
49
|
+
import yaml
|
|
50
|
+
|
|
51
|
+
from spl.core.entities.control import DSPLImport
|
|
52
|
+
from spl.core.entities.function import (
|
|
53
|
+
LOCATION_DUNDER_NAME,
|
|
54
|
+
get_dependency_names_from_bytecode,
|
|
55
|
+
serialize_function,
|
|
56
|
+
)
|
|
57
|
+
from spl.core.ir.common import DBase
|
|
58
|
+
from spl.core.ir.parse import _attach, _branch, ir_parse
|
|
59
|
+
from spl.core.ir.unparse import ir_unparse
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# --------------------------------------------------------------------------- #
|
|
63
|
+
# DLocalAlias: rebind an aliased local import after the target is inlined.
|
|
64
|
+
#
|
|
65
|
+
# A helper is always inlined under its real ``def`` name, so a caller that
|
|
66
|
+
# referred to it through an alias (``from helpers import helper as h``) needs
|
|
67
|
+
# that alias rebound in its own scope: ``h = helper``.
|
|
68
|
+
# --------------------------------------------------------------------------- #
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen = True)
|
|
71
|
+
class DLocalAlias(DBase):
|
|
72
|
+
alias: str
|
|
73
|
+
target: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
yaml.add_representer(
|
|
77
|
+
DLocalAlias,
|
|
78
|
+
lambda dumper, data: dumper.represent_mapping('!DLocalAlias', data.__dict__))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
yaml.add_constructor(
|
|
82
|
+
'!DLocalAlias',
|
|
83
|
+
lambda loader, node: DLocalAlias(**loader.construct_mapping(node)))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@ir_unparse.register(lambda x: isinstance(x, DLocalAlias))
|
|
87
|
+
def _ir_unparse__local_alias(x: DLocalAlias, source: Path) -> Generator[ast.stmt]:
|
|
88
|
+
yield ast.Assign(
|
|
89
|
+
targets = [ast.Name(id = x.alias, ctx = ast.Store())],
|
|
90
|
+
value = ast.Name(id = x.target, ctx = ast.Load()))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------- #
|
|
94
|
+
# Locality detection: is a module first-party user code, or third-party?
|
|
95
|
+
# --------------------------------------------------------------------------- #
|
|
96
|
+
|
|
97
|
+
@lru_cache(maxsize = None)
|
|
98
|
+
def _environment_roots() -> tuple[Path, ...]:
|
|
99
|
+
"""Filesystem roots that mark an interpreter-managed / third-party module."""
|
|
100
|
+
roots: set[Path] = set()
|
|
101
|
+
for key in ('stdlib', 'platstdlib', 'purelib', 'platlib'):
|
|
102
|
+
value = sysconfig.get_paths().get(key)
|
|
103
|
+
if value:
|
|
104
|
+
roots.add(Path(value))
|
|
105
|
+
for value in (sys.prefix, sys.base_prefix, sys.exec_prefix, sys.base_exec_prefix):
|
|
106
|
+
if value:
|
|
107
|
+
roots.add(Path(value))
|
|
108
|
+
|
|
109
|
+
resolved: set[Path] = set()
|
|
110
|
+
for root in roots:
|
|
111
|
+
try:
|
|
112
|
+
resolved.add(root.resolve())
|
|
113
|
+
except OSError:
|
|
114
|
+
continue
|
|
115
|
+
return tuple(resolved)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_within(path: Path, roots: tuple[Path, ...]) -> bool:
|
|
119
|
+
try:
|
|
120
|
+
resolved = path.resolve()
|
|
121
|
+
except OSError:
|
|
122
|
+
return False
|
|
123
|
+
for root in roots:
|
|
124
|
+
try:
|
|
125
|
+
resolved.relative_to(root)
|
|
126
|
+
return True
|
|
127
|
+
except ValueError:
|
|
128
|
+
continue
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@lru_cache(maxsize = None)
|
|
133
|
+
def _module_is_local(module_name: str) -> bool:
|
|
134
|
+
"""Return ``True`` for first-party modules that we should inline.
|
|
135
|
+
|
|
136
|
+
The decision is made by *file location*, not by whether the module belongs
|
|
137
|
+
to an installed distribution. This is deliberate. A package that is being
|
|
138
|
+
actively developed is frequently installed in editable mode
|
|
139
|
+
(``pip install -e .``), which registers it as a distribution even though its
|
|
140
|
+
source still lives in the working tree and is not published anywhere. Such
|
|
141
|
+
a package must be inlined ("gutted") exactly like loose files in a folder --
|
|
142
|
+
referencing it as a pip dependency would produce an artifact that cannot be
|
|
143
|
+
reconstructed on another machine.
|
|
144
|
+
|
|
145
|
+
So both first-party shapes are treated identically:
|
|
146
|
+
|
|
147
|
+
* loose modules in a directory on ``sys.path``;
|
|
148
|
+
* an unpublished package under development (incl. an editable install).
|
|
149
|
+
|
|
150
|
+
Only genuinely environment-installed code -- the standard library and
|
|
151
|
+
packages that live under ``site-packages`` / the interpreter prefix -- is
|
|
152
|
+
left to the import handlers so it can be referenced + pinned as a
|
|
153
|
+
``DDistribution``.
|
|
154
|
+
"""
|
|
155
|
+
if not module_name or module_name == '__main__':
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
top_level = module_name.split('.')[0]
|
|
159
|
+
if top_level in sys.stdlib_module_names or top_level in sys.builtin_module_names:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
module = sys.modules.get(module_name)
|
|
163
|
+
file = getattr(module, '__file__', None) if module is not None else None
|
|
164
|
+
if not file:
|
|
165
|
+
# No importable source on disk (builtin / namespace / C-extension):
|
|
166
|
+
# we could not inline it anyway, so leave it to the import handlers.
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
path = Path(file)
|
|
170
|
+
if 'site-packages' in path.parts or 'dist-packages' in path.parts:
|
|
171
|
+
return False
|
|
172
|
+
if _is_within(path, _environment_roots()):
|
|
173
|
+
return False
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# --------------------------------------------------------------------------- #
|
|
178
|
+
# Source parsing + dependency extraction.
|
|
179
|
+
#
|
|
180
|
+
# Dependencies are resolved against ``func.__globals__`` -- the namespace of the
|
|
181
|
+
# file that defines the function -- never the interpreter call stack. That is
|
|
182
|
+
# precisely what lets the recursion cross file boundaries: each helper resolves
|
|
183
|
+
# the names *it* references in *its own* module.
|
|
184
|
+
# --------------------------------------------------------------------------- #
|
|
185
|
+
|
|
186
|
+
@lru_cache(maxsize = None)
|
|
187
|
+
def _function_def(func: FunctionType) -> ast.FunctionDef | None:
|
|
188
|
+
"""Parse a single ``def`` from a function's source, or ``None`` if unusable."""
|
|
189
|
+
try:
|
|
190
|
+
source = textwrap.dedent(inspect.getsource(func))
|
|
191
|
+
except (OSError, TypeError):
|
|
192
|
+
return None
|
|
193
|
+
try:
|
|
194
|
+
module = ast.parse(source)
|
|
195
|
+
except SyntaxError:
|
|
196
|
+
return None
|
|
197
|
+
body = module.body
|
|
198
|
+
if len(body) == 1 and isinstance(body[0], ast.FunctionDef):
|
|
199
|
+
return body[0]
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def is_inlinable_local_function(x: Any) -> bool:
|
|
204
|
+
"""Dispatch predicate: a pure-Python function from a first-party local file.
|
|
205
|
+
|
|
206
|
+
Evaluated by the dispatcher against *every* object, so it must be cheap for
|
|
207
|
+
the common (non-function) case and must never raise.
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
if not isinstance(x, FunctionType):
|
|
211
|
+
return False
|
|
212
|
+
module_name = getattr(x, '__module__', None)
|
|
213
|
+
if module_name in (None, '__main__'):
|
|
214
|
+
return False
|
|
215
|
+
if not _module_is_local(module_name):
|
|
216
|
+
return False
|
|
217
|
+
return _function_def(x) is not None
|
|
218
|
+
except Exception: # noqa: BLE001 - a predicate must not abort dispatch.
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _annotation_sources(tree: ast.FunctionDef) -> Generator[str]:
|
|
223
|
+
"""Source text of argument annotations and defaults (parity with __main__)."""
|
|
224
|
+
for argument in tree.args.args:
|
|
225
|
+
if argument.annotation is not None:
|
|
226
|
+
yield ast.unparse(argument.annotation)
|
|
227
|
+
for default in tree.args.defaults:
|
|
228
|
+
yield ast.unparse(default)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _referenced_names(func: FunctionType, tree: ast.FunctionDef) -> list[str]:
|
|
232
|
+
names: set[str] = set(get_dependency_names_from_bytecode(func))
|
|
233
|
+
for source in _annotation_sources(tree):
|
|
234
|
+
names.update(get_dependency_names_from_bytecode(source))
|
|
235
|
+
return sorted(name for name in names if name not in vars(builtins))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _local_dependencies(
|
|
239
|
+
func: FunctionType,
|
|
240
|
+
tree: ast.FunctionDef) -> Generator[Any]:
|
|
241
|
+
"""Yield IR for every name the function reads, resolved in its own module."""
|
|
242
|
+
namespace = func.__globals__
|
|
243
|
+
names = _referenced_names(func, tree)
|
|
244
|
+
|
|
245
|
+
missing = [name for name in names if name not in namespace]
|
|
246
|
+
if missing:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
'cannot inline local function {!r}: undefined names {}'.format(
|
|
249
|
+
func.__qualname__, ', '.join(missing)))
|
|
250
|
+
|
|
251
|
+
for name in names:
|
|
252
|
+
# Aliased imports are rebound by the handler below (which sees both the
|
|
253
|
+
# bound name and the real def name), so this stays a plain dispatch.
|
|
254
|
+
yield ir_parse(namespace[name], name)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# --------------------------------------------------------------------------- #
|
|
258
|
+
# ir_parse handler: inline the local function instead of importing it.
|
|
259
|
+
# --------------------------------------------------------------------------- #
|
|
260
|
+
|
|
261
|
+
@ir_parse.register(is_inlinable_local_function)
|
|
262
|
+
def _ir_parse__local_function(
|
|
263
|
+
x: FunctionType,
|
|
264
|
+
name: str | None = None):
|
|
265
|
+
|
|
266
|
+
if hasattr(x, LOCATION_DUNDER_NAME):
|
|
267
|
+
# Already an spl-imported function: reference its source file, do not
|
|
268
|
+
# inline it again (mirrors the __main__ handler).
|
|
269
|
+
return _attach(chain([DSPLImport(*getattr(x, LOCATION_DUNDER_NAME))]))
|
|
270
|
+
|
|
271
|
+
tree = _function_def(x)
|
|
272
|
+
branch = _branch(
|
|
273
|
+
x,
|
|
274
|
+
lambda: serialize_function(x, tree),
|
|
275
|
+
lambda _frame_offset: _local_dependencies(x, tree))
|
|
276
|
+
|
|
277
|
+
# When this helper is reached through an alias (``import helper as h``) the
|
|
278
|
+
# calling body still refers to it as ``h``, yet it is inlined under its real
|
|
279
|
+
# def name. Bundle a local rebind (``h = helper``) next to the inlined
|
|
280
|
+
# function so the alias lands in *the caller's* scope. Because the alias is
|
|
281
|
+
# attached here -- not in the caller's dependency walk -- this works for any
|
|
282
|
+
# caller, including a ``__main__`` entry function whose name resolution also
|
|
283
|
+
# routes through ``ir_parse`` and therefore through this handler.
|
|
284
|
+
if name is not None and name != tree.name:
|
|
285
|
+
return _attach([branch, DLocalAlias(alias = name, target = tree.name)])
|
|
286
|
+
return branch
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from itertools import chain
|
|
2
|
+
from types import UnionType
|
|
3
|
+
|
|
4
|
+
from spl.core.ir.parse import _attach, ir_parse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@ir_parse.register(lambda x: x is None)
|
|
8
|
+
def _ir_parse__none(x, name):
|
|
9
|
+
return _attach(iter(int, int()))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@ir_parse.register(lambda x: isinstance(x, UnionType))
|
|
13
|
+
def _ir_parse__union(x, name):
|
|
14
|
+
return _attach(chain.from_iterable(map(ir_parse, x.__args__)))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import sys
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import FunctionType, ModuleType
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from spl.core.entities.distribution import get_dependencies_from_distribution
|
|
11
|
+
from spl.core.ir.common import DBase
|
|
12
|
+
from spl.core.ir.parse import _attach, ir_parse
|
|
13
|
+
from spl.core.ir.unparse import ir_unparse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen = True)
|
|
17
|
+
class DImport(DBase):
|
|
18
|
+
module: str
|
|
19
|
+
alias: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
yaml.add_representer(
|
|
23
|
+
DImport,
|
|
24
|
+
lambda dumper, data: dumper.represent_mapping('!DImport', data.__dict__))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
yaml.add_constructor(
|
|
28
|
+
'!DImport',
|
|
29
|
+
lambda loader, node: DImport(**loader.construct_mapping(node)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen = True)
|
|
33
|
+
class DImportFrom(DBase):
|
|
34
|
+
module: str
|
|
35
|
+
target: str
|
|
36
|
+
alias: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
yaml.add_representer(
|
|
40
|
+
DImportFrom,
|
|
41
|
+
lambda dumper, data: dumper.represent_mapping('!DImportFrom', data.__dict__))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
yaml.add_constructor(
|
|
45
|
+
'!DImportFrom',
|
|
46
|
+
lambda loader, node: DImportFrom(**loader.construct_mapping(node)))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@ir_parse.register(lambda x: isinstance(x, ModuleType))
|
|
50
|
+
def _ir_parse__module_import(x: ModuleType, name: str | None = None):
|
|
51
|
+
return _attach(chain(
|
|
52
|
+
[DImport(
|
|
53
|
+
module = x.__name__,
|
|
54
|
+
alias = None if x.__name__ == name else name)],
|
|
55
|
+
get_dependencies_from_distribution(x)))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@ir_parse.register(
|
|
59
|
+
lambda x: (
|
|
60
|
+
(isinstance(x, FunctionType) or isinstance(x, type)) and
|
|
61
|
+
(hasattr(x, '__module__'))))
|
|
62
|
+
def _ir_parse__object_import(x: type | FunctionType, name: str | None = None):
|
|
63
|
+
m = sys.modules[x.__module__]
|
|
64
|
+
|
|
65
|
+
match [k for k, v in m.__dict__.items() if v == x]:
|
|
66
|
+
case []:
|
|
67
|
+
raise ValueError('variable {} not found in module {}'.format(name, m.__name__))
|
|
68
|
+
|
|
69
|
+
case [orig_name, *_]:
|
|
70
|
+
return _attach(chain(
|
|
71
|
+
[DImportFrom(
|
|
72
|
+
module = m.__name__,
|
|
73
|
+
target = orig_name,
|
|
74
|
+
alias = None if orig_name == name else name)],
|
|
75
|
+
get_dependencies_from_distribution(m)))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@ir_unparse.register(lambda x: isinstance(x, DImport))
|
|
79
|
+
def _ir_unparse__module_import(x: DImport, source: Path):
|
|
80
|
+
yield ast.Import(
|
|
81
|
+
names = [ast.alias(name = x.module, asname = x.alias)])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@ir_unparse.register(lambda x: isinstance(x, DImportFrom))
|
|
85
|
+
def _ir_unparse__object_import(x: DImportFrom, source: Path):
|
|
86
|
+
yield ast.ImportFrom(
|
|
87
|
+
module = x.module,
|
|
88
|
+
names = [ast.alias(name = x.target, asname = x.alias)])
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Generator
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from spl.core.ir.common import DBase
|
|
10
|
+
from spl.core.ir.parse import _branch, ir_parse
|
|
11
|
+
from spl.core.ir.unparse import ir_unparse
|
|
12
|
+
|
|
13
|
+
DEFAULT_PORT = 'default'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_formatted_output_ref_string(name: str, value: str) -> None:
|
|
17
|
+
if not isinstance(value, str):
|
|
18
|
+
raise TypeError('formatted output ref {} must be a string'.format(name))
|
|
19
|
+
if not value:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
'formatted output ref {} must be a non-empty string'.format(name))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen = True)
|
|
25
|
+
class Port:
|
|
26
|
+
name: str
|
|
27
|
+
typ_: str | None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen = True)
|
|
31
|
+
class InputPort(Port):
|
|
32
|
+
default: str | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen = True)
|
|
36
|
+
class OutputPort(Port):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen = True)
|
|
41
|
+
class Node:
|
|
42
|
+
uuid: UUID
|
|
43
|
+
inputs: list[InputPort]
|
|
44
|
+
outputs: list[OutputPort]
|
|
45
|
+
|
|
46
|
+
def __init__(self, inputs, outputs, uuid: UUID | None = None):
|
|
47
|
+
uuid = uuid or uuid4()
|
|
48
|
+
super().__init__()
|
|
49
|
+
object.__setattr__(self, 'uuid', uuid)
|
|
50
|
+
object.__setattr__(self, 'inputs', inputs)
|
|
51
|
+
object.__setattr__(self, 'outputs', outputs)
|
|
52
|
+
|
|
53
|
+
def __hash__(self):
|
|
54
|
+
return hash(self.uuid)
|
|
55
|
+
|
|
56
|
+
def get_input_port(self, port_name):
|
|
57
|
+
return next(iter(filter(lambda p: p.name == port_name, self.inputs)))
|
|
58
|
+
|
|
59
|
+
def get_output_port(self, port_name = DEFAULT_PORT):
|
|
60
|
+
return next(iter(filter(lambda p: p.name == port_name, self.outputs)))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen = True)
|
|
64
|
+
class NodeInputRef:
|
|
65
|
+
node: Node
|
|
66
|
+
port: InputPort
|
|
67
|
+
|
|
68
|
+
def __repr__(self):
|
|
69
|
+
return self.node.__repr__() + ':' + self.port.name
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen = True)
|
|
72
|
+
class DNodeInputRef(DBase):
|
|
73
|
+
uuid: str
|
|
74
|
+
port: str
|
|
75
|
+
|
|
76
|
+
yaml.add_representer(
|
|
77
|
+
DNodeInputRef,
|
|
78
|
+
lambda dumper, data: dumper.represent_mapping('!DNodeInputRef', data.__dict__))
|
|
79
|
+
|
|
80
|
+
yaml.add_constructor(
|
|
81
|
+
'!DNodeInputRef',
|
|
82
|
+
lambda loader, node: DNodeInputRef(**loader.construct_mapping(node)))
|
|
83
|
+
|
|
84
|
+
@ir_parse.register(
|
|
85
|
+
lambda x: isinstance(x, NodeInputRef))
|
|
86
|
+
def _ir_parse__node_input_ref(
|
|
87
|
+
x: NodeInputRef,
|
|
88
|
+
name: str | None = None):
|
|
89
|
+
return _branch(
|
|
90
|
+
x,
|
|
91
|
+
lambda: DNodeInputRef(
|
|
92
|
+
uuid = str(x.node.uuid),
|
|
93
|
+
port = str(x.port.name)),
|
|
94
|
+
lambda frame_offset: [])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@ir_unparse.register(
|
|
98
|
+
lambda x: isinstance(x, DNodeInputRef))
|
|
99
|
+
def _ir_unparse__node_input_ref(x: DNodeInputRef, source: Path) -> Generator[ast.stmt]:
|
|
100
|
+
yield ast.Assign(
|
|
101
|
+
targets = [ast.Name(id = '_link_from', ctx = ast.Store())],
|
|
102
|
+
value = ast.Call(
|
|
103
|
+
func = ast.Name(id = 'NodeInputRef', ctx = ast.Load()),
|
|
104
|
+
keywords = [
|
|
105
|
+
ast.keyword(
|
|
106
|
+
arg = 'node',
|
|
107
|
+
value = ast.Subscript(
|
|
108
|
+
value = ast.Name(id = '_nodes', ctx = ast.Load()),
|
|
109
|
+
slice = ast.Call(
|
|
110
|
+
func = ast.Name(id = 'UUID', ctx = ast.Load()),
|
|
111
|
+
args = [ast.Constant(value = x.uuid)]),
|
|
112
|
+
ctx = ast.Load())),
|
|
113
|
+
ast.keyword(
|
|
114
|
+
arg = 'port',
|
|
115
|
+
value = ast.Call(
|
|
116
|
+
func = ast.Attribute(
|
|
117
|
+
value = ast.Subscript(
|
|
118
|
+
value = ast.Name(id = '_nodes', ctx = ast.Load()),
|
|
119
|
+
slice = ast.Call(
|
|
120
|
+
func = ast.Name(id = 'UUID', ctx = ast.Load()),
|
|
121
|
+
args = [ast.Constant(value = x.uuid)]),
|
|
122
|
+
ctx = ast.Load()),
|
|
123
|
+
attr = 'get_input_port',
|
|
124
|
+
ctx = ast.Load()),
|
|
125
|
+
args = [ast.Constant(value = x.port)]))]))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen = True)
|
|
129
|
+
class NodeOutputRef:
|
|
130
|
+
node: Node
|
|
131
|
+
port: OutputPort
|
|
132
|
+
|
|
133
|
+
def __repr__(self):
|
|
134
|
+
return self.node.__repr__() + ':' + self.port.name
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen = True)
|
|
137
|
+
class DNodeOutputRef(DBase):
|
|
138
|
+
uuid: str
|
|
139
|
+
port: str
|
|
140
|
+
|
|
141
|
+
yaml.add_representer(
|
|
142
|
+
DNodeOutputRef,
|
|
143
|
+
lambda dumper, data: dumper.represent_mapping('!DNodeOutputRef', data.__dict__))
|
|
144
|
+
|
|
145
|
+
yaml.add_constructor(
|
|
146
|
+
'!DNodeOutputRef',
|
|
147
|
+
lambda loader, node: DNodeOutputRef(**loader.construct_mapping(node)))
|
|
148
|
+
|
|
149
|
+
@ir_parse.register(
|
|
150
|
+
lambda x: isinstance(x, NodeOutputRef))
|
|
151
|
+
def _ir_parse__node_output_ref(
|
|
152
|
+
x: NodeOutputRef,
|
|
153
|
+
name: str | None = None):
|
|
154
|
+
return _branch(
|
|
155
|
+
x,
|
|
156
|
+
lambda: DNodeOutputRef(
|
|
157
|
+
uuid = str(x.node.uuid),
|
|
158
|
+
port = str(x.port.name)),
|
|
159
|
+
lambda frame_offset: [])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@ir_unparse.register(
|
|
163
|
+
lambda x: isinstance(x, DNodeOutputRef))
|
|
164
|
+
def _ir_unparse__node_output_ref(x: DNodeOutputRef, source: Path) -> Generator[ast.stmt]:
|
|
165
|
+
yield ast.Assign(
|
|
166
|
+
targets = [ast.Name(id = '_link_to', ctx = ast.Store())],
|
|
167
|
+
value = ast.Call(
|
|
168
|
+
func = ast.Name(id = 'NodeOutputRef', ctx = ast.Load()),
|
|
169
|
+
keywords = [
|
|
170
|
+
ast.keyword(
|
|
171
|
+
arg = 'node',
|
|
172
|
+
value = ast.Subscript(
|
|
173
|
+
value = ast.Name(id = '_nodes', ctx = ast.Load()),
|
|
174
|
+
slice = ast.Call(
|
|
175
|
+
func = ast.Name(id = 'UUID', ctx = ast.Load()),
|
|
176
|
+
args = [ast.Constant(value = x.uuid)]),
|
|
177
|
+
ctx = ast.Load())),
|
|
178
|
+
ast.keyword(
|
|
179
|
+
arg = 'port',
|
|
180
|
+
value = ast.Call(
|
|
181
|
+
func = ast.Attribute(
|
|
182
|
+
value = ast.Subscript(
|
|
183
|
+
value = ast.Name(id = '_nodes', ctx = ast.Load()),
|
|
184
|
+
slice = ast.Call(
|
|
185
|
+
func = ast.Name(id = 'UUID', ctx = ast.Load()),
|
|
186
|
+
args = [ast.Constant(value = x.uuid)]),
|
|
187
|
+
ctx = ast.Load()),
|
|
188
|
+
attr = 'get_output_port',
|
|
189
|
+
ctx = ast.Load()),
|
|
190
|
+
args = [ast.Constant(value = x.port)]))]))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen = True)
|
|
194
|
+
class FormattedOutputRef:
|
|
195
|
+
"""Output reference with an edge-local artifact format override."""
|
|
196
|
+
|
|
197
|
+
out_ref: NodeOutputRef
|
|
198
|
+
format: str
|
|
199
|
+
|
|
200
|
+
def __post_init__(self) -> None:
|
|
201
|
+
if not isinstance(self.out_ref, NodeOutputRef):
|
|
202
|
+
raise TypeError('formatted output ref out_ref must be NodeOutputRef')
|
|
203
|
+
_validate_formatted_output_ref_string('format', self.format)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen = True)
|
|
207
|
+
class DFormattedOutputRef(DBase):
|
|
208
|
+
"""Serialized FormattedOutputRef value for pipeline YAML."""
|
|
209
|
+
|
|
210
|
+
uuid: str
|
|
211
|
+
port: str
|
|
212
|
+
format: str
|
|
213
|
+
|
|
214
|
+
def __post_init__(self) -> None:
|
|
215
|
+
_validate_formatted_output_ref_string('uuid', self.uuid)
|
|
216
|
+
_validate_formatted_output_ref_string('port', self.port)
|
|
217
|
+
_validate_formatted_output_ref_string('format', self.format)
|
|
218
|
+
|
|
219
|
+
yaml.add_representer(
|
|
220
|
+
DFormattedOutputRef,
|
|
221
|
+
lambda dumper, data: dumper.represent_mapping('!DFormattedOutputRef', data.__dict__))
|
|
222
|
+
|
|
223
|
+
def _construct_dformatted_output_ref(loader: Any, node: Any) -> DFormattedOutputRef:
|
|
224
|
+
return DFormattedOutputRef(**loader.construct_mapping(node))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
yaml.add_constructor(
|
|
228
|
+
'!DFormattedOutputRef',
|
|
229
|
+
_construct_dformatted_output_ref)
|
|
230
|
+
|
|
231
|
+
@ir_parse.register(
|
|
232
|
+
lambda x: isinstance(x, FormattedOutputRef))
|
|
233
|
+
def _ir_parse__formatted_output_ref(
|
|
234
|
+
x: FormattedOutputRef,
|
|
235
|
+
name: str | None = None) -> _branch:
|
|
236
|
+
def mk_dependencies(frame_offset: int) -> Generator[Any]:
|
|
237
|
+
yield from []
|
|
238
|
+
|
|
239
|
+
return _branch(
|
|
240
|
+
x,
|
|
241
|
+
lambda: DFormattedOutputRef(
|
|
242
|
+
uuid = str(x.out_ref.node.uuid),
|
|
243
|
+
port = str(x.out_ref.port.name),
|
|
244
|
+
format = x.format),
|
|
245
|
+
mk_dependencies)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@ir_unparse.register(
|
|
249
|
+
lambda x: isinstance(x, DFormattedOutputRef))
|
|
250
|
+
def _ir_unparse__formatted_output_ref(
|
|
251
|
+
x: DFormattedOutputRef,
|
|
252
|
+
source: Path) -> Generator[ast.stmt]:
|
|
253
|
+
yield ast.Assign(
|
|
254
|
+
targets = [ast.Name(id = '_link_to', ctx = ast.Store())],
|
|
255
|
+
value = ast.Call(
|
|
256
|
+
func = ast.Name(id = 'FormattedOutputRef', ctx = ast.Load()),
|
|
257
|
+
keywords = [
|
|
258
|
+
ast.keyword(
|
|
259
|
+
arg = 'out_ref',
|
|
260
|
+
value = ast.Call(
|
|
261
|
+
func = ast.Name(id = 'NodeOutputRef', ctx = ast.Load()),
|
|
262
|
+
keywords = [
|
|
263
|
+
ast.keyword(
|
|
264
|
+
arg = 'node',
|
|
265
|
+
value = ast.Subscript(
|
|
266
|
+
value = ast.Name(id = '_nodes', ctx = ast.Load()),
|
|
267
|
+
slice = ast.Call(
|
|
268
|
+
func = ast.Name(id = 'UUID', ctx = ast.Load()),
|
|
269
|
+
args = [ast.Constant(value = x.uuid)]),
|
|
270
|
+
ctx = ast.Load())),
|
|
271
|
+
ast.keyword(
|
|
272
|
+
arg = 'port',
|
|
273
|
+
value = ast.Call(
|
|
274
|
+
func = ast.Attribute(
|
|
275
|
+
value = ast.Subscript(
|
|
276
|
+
value = ast.Name(id = '_nodes', ctx = ast.Load()),
|
|
277
|
+
slice = ast.Call(
|
|
278
|
+
func = ast.Name(id = 'UUID', ctx = ast.Load()),
|
|
279
|
+
args = [ast.Constant(value = x.uuid)]),
|
|
280
|
+
ctx = ast.Load()),
|
|
281
|
+
attr = 'get_output_port',
|
|
282
|
+
ctx = ast.Load()),
|
|
283
|
+
args = [ast.Constant(value = x.port)]))])),
|
|
284
|
+
ast.keyword(
|
|
285
|
+
arg = 'format',
|
|
286
|
+
value = ast.Constant(value = x.format))]))
|