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,141 @@
1
+ import ast
2
+ import hashlib
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Generator
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
+
14
+ _HASH_CHUNK_SIZE = 1024 * 1024
15
+ _SHA256_HEX_DIGITS = set('0123456789abcdefABCDEF')
16
+
17
+
18
+ def _validate_non_empty_string(name: str, value: str) -> None:
19
+ if not isinstance(value, str):
20
+ raise TypeError('artifact ref {} must be a string'.format(name))
21
+ if not value:
22
+ raise ValueError('artifact ref {} must be a non-empty string'.format(name))
23
+
24
+
25
+ def _validate_sha256(value: str) -> None:
26
+ _validate_non_empty_string('sha256', value)
27
+ if len(value) != 64 or any(c not in _SHA256_HEX_DIGITS for c in value):
28
+ raise ValueError('artifact ref sha256 must be a 64-character hex string')
29
+
30
+
31
+ def _validate_size(value: int) -> None:
32
+ if type(value) is not int:
33
+ raise TypeError('artifact ref size must be an integer')
34
+ if value < 0:
35
+ raise ValueError('artifact ref size must be non-negative')
36
+
37
+
38
+ def _validate_artifact_ref(key: str, uri: str, sha256: str, size: int) -> None:
39
+ _validate_non_empty_string('key', key)
40
+ _validate_non_empty_string('uri', uri)
41
+ _validate_sha256(sha256)
42
+ _validate_size(size)
43
+
44
+
45
+ @dataclass(frozen = True)
46
+ class ArtifactRef:
47
+ """Reference to a materialized artifact on the local filesystem."""
48
+
49
+ key: str
50
+ uri: str
51
+ sha256: str
52
+ size: int
53
+
54
+ def __post_init__(self) -> None:
55
+ _validate_artifact_ref(
56
+ key = self.key,
57
+ uri = self.uri,
58
+ sha256 = self.sha256,
59
+ size = self.size)
60
+
61
+
62
+ @dataclass(frozen = True)
63
+ class DArtifactRef(DBase):
64
+ """Serialized ArtifactRef value for pipeline YAML."""
65
+
66
+ key: str
67
+ uri: str
68
+ sha256: str
69
+ size: int
70
+
71
+ def __post_init__(self) -> None:
72
+ _validate_artifact_ref(
73
+ key = self.key,
74
+ uri = self.uri,
75
+ sha256 = self.sha256,
76
+ size = self.size)
77
+
78
+
79
+ yaml.add_representer(
80
+ DArtifactRef,
81
+ lambda dumper, data: dumper.represent_mapping('!DArtifactRef', data.__dict__))
82
+
83
+
84
+ def _construct_dartifact_ref(loader: Any, node: Any) -> DArtifactRef:
85
+ return DArtifactRef(**loader.construct_mapping(node))
86
+
87
+
88
+ yaml.add_constructor(
89
+ '!DArtifactRef',
90
+ _construct_dartifact_ref)
91
+
92
+
93
+ def compute_sha256(path: Path) -> str:
94
+ """Compute a file's SHA-256 digest with chunked reads."""
95
+
96
+ digest = hashlib.sha256()
97
+ with path.open('rb') as f:
98
+ for chunk in iter(lambda: f.read(_HASH_CHUNK_SIZE), b''):
99
+ digest.update(chunk)
100
+ return digest.hexdigest()
101
+
102
+
103
+ def _mk_empty_dependencies(frame_offset: int) -> Generator[DBase]:
104
+ yield from ()
105
+
106
+
107
+ @ir_parse.register(
108
+ lambda x: isinstance(x, ArtifactRef))
109
+ def _ir_parse__artifact_ref(
110
+ x: ArtifactRef,
111
+ name: str | None = None) -> _branch:
112
+ return _branch(
113
+ x,
114
+ lambda: DArtifactRef(
115
+ key = x.key,
116
+ uri = x.uri,
117
+ sha256 = x.sha256,
118
+ size = x.size),
119
+ _mk_empty_dependencies)
120
+
121
+
122
+ @ir_unparse.register(
123
+ lambda x: isinstance(x, DArtifactRef))
124
+ def _ir_unparse__artifact_ref(x: DArtifactRef, source: Path) -> Generator[ast.stmt]:
125
+ yield ast.Assign(
126
+ targets = [ast.Name(id = '_link_to', ctx = ast.Store())],
127
+ value = ast.Call(
128
+ func = ast.Name(id = 'ArtifactRef', ctx = ast.Load()),
129
+ keywords = [
130
+ ast.keyword(
131
+ arg = 'key',
132
+ value = ast.Constant(value = x.key)),
133
+ ast.keyword(
134
+ arg = 'uri',
135
+ value = ast.Constant(value = x.uri)),
136
+ ast.keyword(
137
+ arg = 'sha256',
138
+ value = ast.Constant(value = x.sha256)),
139
+ ast.keyword(
140
+ arg = 'size',
141
+ value = ast.Constant(value = x.size))]))
@@ -0,0 +1,45 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+ from spl.core.ir.common import DBase
7
+ from spl.core.ir.unparse import ir_unparse
8
+
9
+
10
+ @dataclass(frozen = True)
11
+ class DSPLSelfImport(DBase):
12
+ name: str
13
+
14
+ yaml.add_representer(
15
+ DSPLSelfImport,
16
+ lambda dumper, data: dumper.represent_mapping('!DSPLSelfImport', data.__dict__))
17
+
18
+ yaml.add_constructor(
19
+ '!DSPLSelfImport',
20
+ lambda loader, node: DSPLSelfImport(**loader.construct_mapping(node)))
21
+
22
+
23
+ @dataclass(frozen = True)
24
+ class DSPLImport(DBase):
25
+ path: str
26
+ name: str
27
+
28
+ yaml.add_representer(
29
+ DSPLImport,
30
+ lambda dumper, data: dumper.represent_mapping('!DSPLImport', data.__dict__))
31
+
32
+ yaml.add_constructor(
33
+ '!DSPLImport',
34
+ lambda loader, node: DSPLImport(**loader.construct_mapping(node)))
35
+
36
+
37
+ @ir_unparse.register(lambda x: isinstance(x, DSPLSelfImport))
38
+ def _ir_unparse__spl_self_import(x: DSPLSelfImport, source: Path):
39
+ yield from []
40
+
41
+
42
+ @ir_unparse.register(lambda x: isinstance(x, DSPLImport))
43
+ def _ir_unparse__spl_import(x: DSPLImport, source: Path):
44
+ yield from []
45
+
@@ -0,0 +1,65 @@
1
+ import importlib
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from importlib.metadata import PackageNotFoundError, packages_distributions
5
+ from itertools import chain
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+ from typing import Generator
9
+
10
+ import yaml
11
+
12
+ from spl.core.ir.common import DBase
13
+ from spl.core.ir.unparse import ir_unparse
14
+
15
+
16
+ @dataclass(frozen = True)
17
+ class DDistribution(DBase):
18
+ package: str
19
+ version: str
20
+
21
+ def __lt__(self, other):
22
+ return (self.package, self.version) < (other.package, other.version)
23
+
24
+ yaml.add_representer(
25
+ DDistribution,
26
+ lambda dumper, data: dumper.represent_mapping('!DDistribution', data.__dict__))
27
+
28
+ yaml.add_constructor(
29
+ '!DDistribution',
30
+ lambda loader, node: DDistribution(**loader.construct_mapping(node)))
31
+
32
+
33
+ def get_dependencies_from_distribution(module: ModuleType) -> Generator[DDistribution]:
34
+ distributions = packages_distributions()
35
+ if (package := module.__package__):
36
+ for x in set(distributions[package.split('.')[0]]):
37
+ yield DDistribution(
38
+ package = x,
39
+ version = importlib.metadata.version(x))
40
+
41
+
42
+ def validate_distributions(deps, source) -> None:
43
+ distributions = sorted(set(filter(
44
+ lambda x: isinstance(x, DDistribution),
45
+ chain.from_iterable([dependencies for (_, dependencies) in deps]))))
46
+
47
+ for x in distributions:
48
+ try:
49
+ if (version := importlib.metadata.version(x.package)) != x.version:
50
+ logging.warning(
51
+ '{}: distribution mismatch: {} == {} (actual {})'.format(
52
+ source,
53
+ x.package,
54
+ x.version, version))
55
+ except PackageNotFoundError:
56
+ logging.warning(
57
+ '{}: distribution is not found: {} == {}'.format(
58
+ source,
59
+ x.package,
60
+ x.version))
61
+
62
+
63
+ @ir_unparse.register(lambda x: isinstance(x, DDistribution))
64
+ def _ir_unparse__distribution(x: DDistribution, source: Path):
65
+ yield from []
@@ -0,0 +1,254 @@
1
+ import ast
2
+ import builtins
3
+ import dis
4
+ import inspect
5
+ import sys
6
+ import typing
7
+ from dataclasses import dataclass
8
+ from itertools import chain
9
+ from pathlib import Path
10
+ from types import CodeType, FunctionType
11
+ from typing import Annotated, Generator
12
+
13
+ import yaml
14
+
15
+ import spl.core.entities.node as m_node
16
+ from spl.core.entities.control import DSPLImport
17
+ from spl.core.entities.node import DEFAULT_PORT, InputPort, OutputPort
18
+ from spl.core.ir.common import DBase
19
+ from spl.core.ir.parse import _attach, _branch, ir_parse
20
+ from spl.core.ir.unparse import ir_unparse
21
+
22
+ LOCATION_DUNDER_NAME = '__spl_location__'
23
+ METADATA_DUNDER_NAME = '__spl_metadata__'
24
+
25
+
26
+ @dataclass
27
+ class _body: # noqa: N801
28
+ value: str
29
+
30
+ yaml.add_representer(
31
+ _body,
32
+ lambda dumper, data: dumper.represent_scalar('tag:yaml.org,2002:str', data.value, style = '|'))
33
+
34
+
35
+ @dataclass(frozen = True)
36
+ class DFunction(DBase):
37
+ name: str
38
+ body: str
39
+ inputs: list[InputPort]
40
+ outputs: list[OutputPort] | None
41
+
42
+ def __hash__(self):
43
+ return hash((self.name, self.body, tuple(self.inputs), tuple(self.outputs)))
44
+
45
+ def __repr__(self):
46
+ return '{}({})'.format(self.__class__.__name__, repr(self.name))
47
+
48
+
49
+ yaml.add_representer(
50
+ DFunction,
51
+ lambda dumper, data: dumper.represent_mapping('!DFunction', {
52
+ 'name': data.name,
53
+ 'inputs': list(map(
54
+ lambda x: {
55
+ 'name': x.name,
56
+ 'type': x.typ_,
57
+ 'default': x.default}, data.inputs)),
58
+ 'outputs': None if data.outputs is None else list(map(
59
+ lambda x: {
60
+ 'name': x.name,
61
+ 'type': x.typ_}, data.outputs)),
62
+ 'body': _body(data.body)}))
63
+
64
+
65
+ yaml.add_constructor(
66
+ '!DFunction',
67
+ lambda loader, node: (lambda data: DFunction(
68
+ name = data['name'],
69
+ body = data['body'],
70
+ inputs = [
71
+ InputPort(
72
+ name = x['name'],
73
+ typ_ = x['type'],
74
+ default = x.get('default'))
75
+ for x in data['inputs']],
76
+ outputs = [
77
+ OutputPort(
78
+ name = x['name'],
79
+ typ_ = x['type'])
80
+ for x in data['outputs']]))
81
+ (loader.construct_mapping(node, deep = True)))
82
+
83
+
84
+ def get_dependency_names_from_bytecode(f: FunctionType | CodeType):
85
+ for x in dis.Bytecode(f):
86
+ match x.opname:
87
+ case 'LOAD_GLOBAL':
88
+ yield x.argval
89
+ case 'LOAD_NAME':
90
+ yield x.argval
91
+ case 'LOAD_CONST':
92
+ if isinstance(x.argval, CodeType):
93
+ yield from get_dependency_names_from_bytecode(x.argval)
94
+
95
+ def get_dependencies_from_bytecode(
96
+ frame_offset,
97
+ f: FunctionType | CodeType):
98
+ g = sys._getframe(1 + frame_offset).f_globals
99
+
100
+ names = sorted(filter(
101
+ lambda x: x not in vars(builtins),
102
+ set(get_dependency_names_from_bytecode(f))))
103
+
104
+ if missing_names := (set(names) - set(g.keys())):
105
+ raise ValueError('missing names: {}'.format(', '.join(sorted(missing_names))))
106
+
107
+ for name in names:
108
+ yield ir_parse(g[name], name)
109
+
110
+
111
+ def get_dependencies_from_ast(frame_offset, tree: ast.FunctionDef):
112
+ for value in map(ast.unparse, filter(None, [x.annotation for x in tree.args.args])):
113
+ yield from get_dependencies_from_bytecode(
114
+ frame_offset + 1,
115
+ value)
116
+ for value in map(ast.unparse, tree.args.defaults):
117
+ yield from get_dependencies_from_bytecode(
118
+ frame_offset + 1,
119
+ value)
120
+
121
+
122
+ def serialize_function_output(func: FunctionType, tree: ast.FunctionDef):
123
+ name = DEFAULT_PORT
124
+
125
+ return_type = func.__annotations__.get('return')
126
+ if (return_type is not None) and (typing.get_origin(return_type) == Annotated):
127
+ (_, name, *_) = typing.get_args(return_type)
128
+
129
+ return [
130
+ OutputPort(
131
+ name = name,
132
+ typ_ = ast.unparse(tree.returns) if (tree.returns is not None) else None)]
133
+
134
+
135
+ def serialize_function(func: FunctionType, tree: ast.FunctionDef | None = None):
136
+ if tree is None:
137
+ [tree] = ast.parse(inspect.getsource(func)).body
138
+
139
+ args = tree.args
140
+
141
+ return DFunction(
142
+ name = tree.name,
143
+ body = ast.unparse(tree.body),
144
+ inputs = [
145
+ InputPort(
146
+ name = a.arg,
147
+ typ_ = ast.unparse(a.annotation) if a.annotation is not None else None,
148
+ default = ast.unparse(d) if d is not None else None)
149
+ for a, d in zip(
150
+ args.args,
151
+ [*[None] * (len(args.args) - len(args.defaults)), *args.defaults],
152
+ strict = True)],
153
+ outputs = serialize_function_output(func, tree))
154
+
155
+
156
+ @ir_parse.register(
157
+ lambda x: (isinstance(x, FunctionType) and (getattr(x, '__module__', None) == '__main__')))
158
+ def _ir_parse__function(
159
+ x: FunctionType,
160
+ name: str | None = None):
161
+
162
+ if hasattr(x, LOCATION_DUNDER_NAME):
163
+ # We imported this function using SPL, using it's metadata.
164
+ return _attach(chain([DSPLImport(*getattr(x, LOCATION_DUNDER_NAME))]))
165
+
166
+ [tree] = ast.parse(inspect.getsource(x)).body
167
+ return _branch(
168
+ x,
169
+ lambda: serialize_function(x, tree),
170
+ lambda frame_offset: chain(
171
+ get_dependencies_from_bytecode(frame_offset, x),
172
+ get_dependencies_from_ast(frame_offset, tree)))
173
+
174
+
175
+ def get_function_metadata(func: FunctionType):
176
+ if hasattr(func, METADATA_DUNDER_NAME):
177
+ # We imported this function using SPL, using it's metadata.
178
+ return getattr(func, METADATA_DUNDER_NAME)
179
+ return serialize_function(func)
180
+
181
+
182
+ @ir_unparse.register(lambda x: isinstance(x, DFunction))
183
+ def _ir_unparse__function(x: DFunction, source: Path) -> Generator[ast.stmt]:
184
+ yield ast.FunctionDef(
185
+ name = x.name,
186
+ body = ast.parse(x.body).body,
187
+ # TODO: add support for multiple outputs
188
+ returns = ast.parse(x.outputs[0].typ_, mode = 'eval').body if x.outputs[0].typ_ is not None else None,
189
+ args = ast.arguments(
190
+ args = [
191
+ ast.arg(
192
+ arg = port.name,
193
+ annotation = ast.parse(port.typ_, mode = 'eval').body if port.typ_ is not None else None)
194
+ for port in x.inputs],
195
+ defaults = [
196
+ ast.parse(port.default, mode = 'eval').body
197
+ for port in x.inputs
198
+ if port.default is not None]))
199
+
200
+ # Importing helpers
201
+ yield ast.ImportFrom(
202
+ module = m_node.__name__,
203
+ names = [
204
+ ast.alias(name = 'InputPort'),
205
+ ast.alias(name = 'OutputPort')])
206
+
207
+ yield ast.ImportFrom(
208
+ module = __name__,
209
+ names = [
210
+ ast.alias(name = 'DFunction')])
211
+
212
+ # Marking function as spl-imported
213
+ yield ast.Expr(value = ast.Call(
214
+ func = ast.Name(id = 'setattr', ctx = ast.Load()),
215
+ args = [
216
+ ast.Name(id = x.name, ctx = ast.Load()),
217
+ ast.Constant(value = LOCATION_DUNDER_NAME),
218
+ ast.Tuple(elts = [
219
+ ast.Constant(value = str(source.absolute())),
220
+ ast.Constant(value = x.name)])]))
221
+
222
+ # Adding metadata
223
+ yield ast.Expr(value = ast.Call(
224
+ func = ast.Name(id = 'setattr', ctx = ast.Load()),
225
+ args = [
226
+ ast.Name(id = x.name, ctx = ast.Load()),
227
+ ast.Constant(value = METADATA_DUNDER_NAME),
228
+ ast.Call(
229
+ func = ast.Name(id = 'DFunction', ctx = ast.Load()),
230
+ keywords = [
231
+ ast.keyword(arg = 'name', value = ast.Constant(value = x.name)),
232
+ ast.keyword(arg = 'body', value = ast.Constant(value = x.body)),
233
+ ast.keyword(arg = 'inputs', value = ast.List(
234
+ elts = [
235
+ ast.Call(
236
+ func = ast.Name(id = 'InputPort', ctx = ast.Load()),
237
+ keywords = [
238
+ ast.keyword(arg = 'name', value = ast.Constant(value = port.name)),
239
+ ast.keyword(arg = 'typ_', value = ast.Constant(value = port.typ_)),
240
+ ast.keyword(
241
+ arg = 'default',
242
+ value = ast.Constant(value = port.default))])
243
+ for port in x.inputs],
244
+ ctx = ast.Load())),
245
+
246
+ ast.keyword(arg = 'outputs', value = ast.List(
247
+ elts = [
248
+ ast.Call(
249
+ func = ast.Name(id = 'OutputPort', ctx = ast.Load()),
250
+ keywords = [
251
+ ast.keyword(arg = 'name', value = ast.Constant(value = port.name)),
252
+ ast.keyword(arg = 'typ_', value = ast.Constant(value = port.typ_))])
253
+ for port in x.outputs],
254
+ ctx = ast.Load()))])]))