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.
Files changed (74) hide show
  1. spl/__init__.py +14 -0
  2. spl/client.py +1364 -0
  3. spl/core/__init__.py +23 -0
  4. spl/core/common.py +350 -0
  5. spl/core/entities/__init__.py +0 -0
  6. spl/core/entities/adapter.py +210 -0
  7. spl/core/entities/artifact.py +141 -0
  8. spl/core/entities/control.py +45 -0
  9. spl/core/entities/distribution.py +65 -0
  10. spl/core/entities/function.py +254 -0
  11. spl/core/entities/local_function.py +286 -0
  12. spl/core/entities/misc.py +14 -0
  13. spl/core/entities/module.py +88 -0
  14. spl/core/entities/node.py +286 -0
  15. spl/core/entities/node_function.py +79 -0
  16. spl/core/entities/node_remote.py +295 -0
  17. spl/core/entities/pipeline.py +436 -0
  18. spl/core/entities/scalar.py +55 -0
  19. spl/core/ir/__init__.py +0 -0
  20. spl/core/ir/common.py +34 -0
  21. spl/core/ir/parse.py +79 -0
  22. spl/core/ir/unparse.py +29 -0
  23. spl/core/ir/utils.py +163 -0
  24. spl/daemon/__init__.py +23 -0
  25. spl/daemon/__main__.py +11 -0
  26. spl/daemon/cli.py +582 -0
  27. spl/daemon/client.py +43 -0
  28. spl/daemon/docker_environment.py +329 -0
  29. spl/daemon/docker_pool.py +516 -0
  30. spl/daemon/environment.py +228 -0
  31. spl/daemon/environment_base.py +479 -0
  32. spl/daemon/heartbeat_service.py +119 -0
  33. spl/daemon/metadata.py +427 -0
  34. spl/daemon/remote_client.py +457 -0
  35. spl/daemon/repositories/__init__.py +17 -0
  36. spl/daemon/repositories/env.py +323 -0
  37. spl/daemon/repositories/library.py +181 -0
  38. spl/daemon/repositories/object.py +997 -0
  39. spl/daemon/repositories/run.py +279 -0
  40. spl/daemon/repositories/server_connection.py +657 -0
  41. spl/daemon/repositories/sync_event.py +129 -0
  42. spl/daemon/routes/__init__.py +1 -0
  43. spl/daemon/routes/_helpers.py +147 -0
  44. spl/daemon/routes/artifacts.py +77 -0
  45. spl/daemon/routes/diagnostics.py +114 -0
  46. spl/daemon/routes/envs.py +82 -0
  47. spl/daemon/routes/libraries.py +129 -0
  48. spl/daemon/routes/objects.py +174 -0
  49. spl/daemon/routes/remote.py +56 -0
  50. spl/daemon/routes/runs.py +96 -0
  51. spl/daemon/routes/server_connections.py +86 -0
  52. spl/daemon/runtime_backend.py +368 -0
  53. spl/daemon/runtime_config.py +133 -0
  54. spl/daemon/runtime_dependencies.py +459 -0
  55. spl/daemon/secret_store.py +187 -0
  56. spl/daemon/server.py +2224 -0
  57. spl/daemon/server_connection.py +267 -0
  58. spl/daemon/services/__init__.py +1 -0
  59. spl/daemon/services/sync.py +76 -0
  60. spl/daemon/signature.py +376 -0
  61. spl/daemon/storage_base.py +542 -0
  62. spl/daemon/store.py +436 -0
  63. spl/daemon/worker.py +526 -0
  64. spl/daemon_client.py +945 -0
  65. spl/pipeline_widget.py +1452 -0
  66. spl/py.typed +0 -0
  67. spl/server_client.py +787 -0
  68. splime-0.1.2.dist-info/METADATA +189 -0
  69. splime-0.1.2.dist-info/RECORD +74 -0
  70. splime-0.1.2.dist-info/WHEEL +5 -0
  71. splime-0.1.2.dist-info/entry_points.txt +2 -0
  72. splime-0.1.2.dist-info/licenses/LICENSE +201 -0
  73. splime-0.1.2.dist-info/licenses/NOTICE +8 -0
  74. 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))]))