tracdap-runtime 0.6.4__py3-none-any.whl → 0.6.5__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.
- tracdap/rt/_exec/context.py +382 -29
- tracdap/rt/_exec/dev_mode.py +123 -94
- tracdap/rt/_exec/engine.py +120 -9
- tracdap/rt/_exec/functions.py +125 -20
- tracdap/rt/_exec/graph.py +38 -13
- tracdap/rt/_exec/graph_builder.py +120 -9
- tracdap/rt/_impl/data.py +115 -49
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +74 -30
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +120 -2
- tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +12 -10
- tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -2
- tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +29 -0
- tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.pyi +16 -0
- tracdap/rt/_impl/models.py +8 -0
- tracdap/rt/_impl/static_api.py +16 -0
- tracdap/rt/_impl/storage.py +37 -25
- tracdap/rt/_impl/validation.py +76 -7
- tracdap/rt/_plugins/repo_git.py +1 -1
- tracdap/rt/_version.py +1 -1
- tracdap/rt/api/experimental.py +220 -0
- tracdap/rt/api/hook.py +4 -0
- tracdap/rt/api/model_api.py +48 -6
- tracdap/rt/config/__init__.py +2 -2
- tracdap/rt/config/common.py +6 -0
- tracdap/rt/metadata/__init__.py +25 -20
- tracdap/rt/metadata/job.py +54 -0
- tracdap/rt/metadata/model.py +18 -0
- tracdap/rt/metadata/resource.py +24 -0
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/METADATA +3 -1
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/RECORD +33 -29
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/LICENSE +0 -0
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/WHEEL +0 -0
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/top_level.txt +0 -0
tracdap/rt/_exec/functions.py
CHANGED
@@ -23,6 +23,7 @@ import tracdap.rt.api as _api
|
|
23
23
|
import tracdap.rt.config as _config
|
24
24
|
import tracdap.rt.exceptions as _ex
|
25
25
|
import tracdap.rt._exec.context as _ctx
|
26
|
+
import tracdap.rt._exec.graph_builder as _graph
|
26
27
|
import tracdap.rt._impl.config_parser as _cfg_p # noqa
|
27
28
|
import tracdap.rt._impl.type_system as _types # noqa
|
28
29
|
import tracdap.rt._impl.data as _data # noqa
|
@@ -59,6 +60,13 @@ class NodeContext:
|
|
59
60
|
pass
|
60
61
|
|
61
62
|
|
63
|
+
class NodeCallback:
|
64
|
+
|
65
|
+
@abc.abstractmethod
|
66
|
+
def send_graph_updates(self, new_nodes: tp.Dict[NodeId, Node], new_deps: tp.Dict[NodeId, tp.List[Dependency]]):
|
67
|
+
pass
|
68
|
+
|
69
|
+
|
62
70
|
# Helper functions to access the node context (in case the NodeContext interface needs to change)
|
63
71
|
|
64
72
|
def _ctx_lookup(node_id: NodeId[_T], ctx: NodeContext) -> _T:
|
@@ -89,8 +97,15 @@ class NodeFunction(tp.Generic[_T]):
|
|
89
97
|
:py:class:`NodeContext <NodeContext>`
|
90
98
|
"""
|
91
99
|
|
92
|
-
def
|
93
|
-
|
100
|
+
def __init__(self):
|
101
|
+
self.node_callback: tp.Optional[NodeCallback] = None
|
102
|
+
|
103
|
+
def __call__(self, ctx: NodeContext, callback: NodeCallback = None) -> _T:
|
104
|
+
try:
|
105
|
+
self.node_callback = callback
|
106
|
+
return self._execute(ctx)
|
107
|
+
finally:
|
108
|
+
self.node_callback = None
|
94
109
|
|
95
110
|
@abc.abstractmethod
|
96
111
|
def _execute(self, ctx: NodeContext) -> _T:
|
@@ -105,6 +120,7 @@ class NodeFunction(tp.Generic[_T]):
|
|
105
120
|
class NoopFunc(NodeFunction[None]):
|
106
121
|
|
107
122
|
def __init__(self, node: NoopNode):
|
123
|
+
super().__init__()
|
108
124
|
self.node = node
|
109
125
|
|
110
126
|
def _execute(self, _: NodeContext) -> None:
|
@@ -114,6 +130,7 @@ class NoopFunc(NodeFunction[None]):
|
|
114
130
|
class StaticValueFunc(NodeFunction[_T]):
|
115
131
|
|
116
132
|
def __init__(self, node: StaticValueNode[_T]):
|
133
|
+
super().__init__()
|
117
134
|
self.node = node
|
118
135
|
|
119
136
|
def _execute(self, ctx: NodeContext) -> _T:
|
@@ -123,6 +140,7 @@ class StaticValueFunc(NodeFunction[_T]):
|
|
123
140
|
class IdentityFunc(NodeFunction[_T]):
|
124
141
|
|
125
142
|
def __init__(self, node: IdentityNode[_T]):
|
143
|
+
super().__init__()
|
126
144
|
self.node = node
|
127
145
|
|
128
146
|
def _execute(self, ctx: NodeContext) -> _T:
|
@@ -138,6 +156,7 @@ class _ContextPushPopFunc(NodeFunction[Bundle[tp.Any]], abc.ABC):
|
|
138
156
|
_POP = False
|
139
157
|
|
140
158
|
def __init__(self, node: tp.Union[ContextPushNode, ContextPopNode], direction: bool):
|
159
|
+
super().__init__()
|
141
160
|
self.node = node
|
142
161
|
self.direction = direction
|
143
162
|
|
@@ -176,6 +195,7 @@ class ContextPopFunc(_ContextPushPopFunc):
|
|
176
195
|
class KeyedItemFunc(NodeFunction[_T]):
|
177
196
|
|
178
197
|
def __init__(self, node: KeyedItemNode[_T]):
|
198
|
+
super().__init__()
|
179
199
|
self.node = node
|
180
200
|
|
181
201
|
def _execute(self, ctx: NodeContext) -> _T:
|
@@ -184,9 +204,20 @@ class KeyedItemFunc(NodeFunction[_T]):
|
|
184
204
|
return src_item
|
185
205
|
|
186
206
|
|
207
|
+
class RuntimeOutputsFunc(NodeFunction[JobOutputs]):
|
208
|
+
|
209
|
+
def __init__(self, node: RuntimeOutputsNode):
|
210
|
+
super().__init__()
|
211
|
+
self.node = node
|
212
|
+
|
213
|
+
def _execute(self, ctx: NodeContext) -> JobOutputs:
|
214
|
+
return self.node.outputs
|
215
|
+
|
216
|
+
|
187
217
|
class BuildJobResultFunc(NodeFunction[_config.JobResult]):
|
188
218
|
|
189
219
|
def __init__(self, node: BuildJobResultNode):
|
220
|
+
super().__init__()
|
190
221
|
self.node = node
|
191
222
|
|
192
223
|
def _execute(self, ctx: NodeContext) -> _config.JobResult:
|
@@ -197,20 +228,33 @@ class BuildJobResultFunc(NodeFunction[_config.JobResult]):
|
|
197
228
|
|
198
229
|
# TODO: Handle individual failed results
|
199
230
|
|
200
|
-
for obj_id, node_id in self.node.objects.items():
|
231
|
+
for obj_id, node_id in self.node.outputs.objects.items():
|
201
232
|
obj_def = _ctx_lookup(node_id, ctx)
|
202
233
|
job_result.results[obj_id] = obj_def
|
203
234
|
|
204
|
-
for bundle_id in self.node.bundles:
|
235
|
+
for bundle_id in self.node.outputs.bundles:
|
205
236
|
bundle = _ctx_lookup(bundle_id, ctx)
|
206
237
|
job_result.results.update(bundle.items())
|
207
238
|
|
239
|
+
if self.node.runtime_outputs is not None:
|
240
|
+
|
241
|
+
runtime_outputs = _ctx_lookup(self.node.runtime_outputs, ctx)
|
242
|
+
|
243
|
+
for obj_id, node_id in runtime_outputs.objects.items():
|
244
|
+
obj_def = _ctx_lookup(node_id, ctx)
|
245
|
+
job_result.results[obj_id] = obj_def
|
246
|
+
|
247
|
+
for bundle_id in runtime_outputs.bundles:
|
248
|
+
bundle = _ctx_lookup(bundle_id, ctx)
|
249
|
+
job_result.results.update(bundle.items())
|
250
|
+
|
208
251
|
return job_result
|
209
252
|
|
210
253
|
|
211
254
|
class SaveJobResultFunc(NodeFunction[None]):
|
212
255
|
|
213
256
|
def __init__(self, node: SaveJobResultNode):
|
257
|
+
super().__init__()
|
214
258
|
self.node = node
|
215
259
|
|
216
260
|
def _execute(self, ctx: NodeContext) -> None:
|
@@ -241,6 +285,7 @@ class SaveJobResultFunc(NodeFunction[None]):
|
|
241
285
|
class DataViewFunc(NodeFunction[_data.DataView]):
|
242
286
|
|
243
287
|
def __init__(self, node: DataViewNode):
|
288
|
+
super().__init__()
|
244
289
|
self.node = node
|
245
290
|
|
246
291
|
def _execute(self, ctx: NodeContext) -> _data.DataView:
|
@@ -267,6 +312,7 @@ class DataViewFunc(NodeFunction[_data.DataView]):
|
|
267
312
|
class DataItemFunc(NodeFunction[_data.DataItem]):
|
268
313
|
|
269
314
|
def __init__(self, node: DataItemNode):
|
315
|
+
super().__init__()
|
270
316
|
self.node = node
|
271
317
|
|
272
318
|
def _execute(self, ctx: NodeContext) -> _data.DataItem:
|
@@ -290,6 +336,7 @@ class DataItemFunc(NodeFunction[_data.DataItem]):
|
|
290
336
|
class DataResultFunc(NodeFunction[ObjectBundle]):
|
291
337
|
|
292
338
|
def __init__(self, node: DataResultNode):
|
339
|
+
super().__init__()
|
293
340
|
self.node = node
|
294
341
|
|
295
342
|
def _execute(self, ctx: NodeContext) -> ObjectBundle:
|
@@ -324,6 +371,7 @@ class DynamicDataSpecFunc(NodeFunction[_data.DataSpec]):
|
|
324
371
|
RANDOM.seed()
|
325
372
|
|
326
373
|
def __init__(self, node: DynamicDataSpecNode, storage: _storage.StorageManager):
|
374
|
+
super().__init__()
|
327
375
|
self.node = node
|
328
376
|
self.storage = storage
|
329
377
|
|
@@ -434,7 +482,7 @@ class _LoadSaveDataFunc(abc.ABC):
|
|
434
482
|
return copy_
|
435
483
|
|
436
484
|
|
437
|
-
class LoadDataFunc(NodeFunction[_data.DataItem],
|
485
|
+
class LoadDataFunc( _LoadSaveDataFunc, NodeFunction[_data.DataItem],):
|
438
486
|
|
439
487
|
def __init__(self, node: LoadDataNode, storage: _storage.StorageManager):
|
440
488
|
super().__init__(storage)
|
@@ -463,7 +511,7 @@ class LoadDataFunc(NodeFunction[_data.DataItem], _LoadSaveDataFunc):
|
|
463
511
|
return _data.DataItem(table.schema, table)
|
464
512
|
|
465
513
|
|
466
|
-
class SaveDataFunc(NodeFunction[None]
|
514
|
+
class SaveDataFunc(_LoadSaveDataFunc, NodeFunction[None]):
|
467
515
|
|
468
516
|
def __init__(self, node: SaveDataNode, storage: _storage.StorageManager):
|
469
517
|
super().__init__(storage)
|
@@ -518,6 +566,7 @@ def _model_def_for_import(import_details: meta.ImportModelJob):
|
|
518
566
|
class ImportModelFunc(NodeFunction[meta.ObjectDefinition]):
|
519
567
|
|
520
568
|
def __init__(self, node: ImportModelNode, models: _models.ModelLoader):
|
569
|
+
super().__init__()
|
521
570
|
self.node = node
|
522
571
|
self._models = models
|
523
572
|
|
@@ -535,11 +584,17 @@ class ImportModelFunc(NodeFunction[meta.ObjectDefinition]):
|
|
535
584
|
|
536
585
|
class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
|
537
586
|
|
538
|
-
def __init__(
|
587
|
+
def __init__(
|
588
|
+
self, node: RunModelNode,
|
589
|
+
model_class: _api.TracModel.__class__,
|
590
|
+
checkout_directory: pathlib.Path,
|
591
|
+
storage_manager: _storage.StorageManager):
|
592
|
+
|
539
593
|
super().__init__()
|
540
594
|
self.node = node
|
541
595
|
self.model_class = model_class
|
542
596
|
self.checkout_directory = checkout_directory
|
597
|
+
self.storage_manager = storage_manager
|
543
598
|
|
544
599
|
def _execute(self, ctx: NodeContext) -> Bundle[_data.DataView]:
|
545
600
|
|
@@ -550,23 +605,37 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
|
|
550
605
|
# Still, if any nodes are missing or have the wrong type TracContextImpl will raise ERuntimeValidation
|
551
606
|
|
552
607
|
local_ctx = {}
|
608
|
+
dynamic_outputs = []
|
553
609
|
|
554
610
|
for node_id, node_result in _ctx_iter_items(ctx):
|
611
|
+
if node_id.namespace == self.node.id.namespace:
|
612
|
+
if node_id.name in model_def.parameters or node_id.name in model_def.inputs:
|
613
|
+
local_ctx[node_id.name] = node_result
|
555
614
|
|
556
|
-
|
557
|
-
continue
|
615
|
+
# Set up access to external storage if required
|
558
616
|
|
559
|
-
|
560
|
-
param_name = node_id.name
|
561
|
-
local_ctx[param_name] = node_result
|
617
|
+
storage_map = {}
|
562
618
|
|
563
|
-
|
564
|
-
|
565
|
-
|
619
|
+
if self.node.storage_access:
|
620
|
+
write_access = True if self.node.model_def.modelType == meta.ModelType.DATA_EXPORT_MODEL else False
|
621
|
+
for storage_key in self.node.storage_access:
|
622
|
+
if self.storage_manager.has_file_storage(storage_key, external=True):
|
623
|
+
storage_impl = self.storage_manager.get_file_storage(storage_key, external=True)
|
624
|
+
storage = _ctx.TracFileStorageImpl(storage_key, storage_impl, write_access, self.checkout_directory)
|
625
|
+
storage_map[storage_key] = storage
|
566
626
|
|
567
627
|
# Run the model against the mapped local context
|
568
628
|
|
569
|
-
|
629
|
+
if model_def.modelType in [meta.ModelType.DATA_IMPORT_MODEL, meta.ModelType.DATA_EXPORT_MODEL]:
|
630
|
+
trac_ctx = _ctx.TracDataContextImpl(
|
631
|
+
self.node.model_def, self.model_class,
|
632
|
+
local_ctx, dynamic_outputs, storage_map,
|
633
|
+
self.checkout_directory)
|
634
|
+
else:
|
635
|
+
trac_ctx = _ctx.TracContextImpl(
|
636
|
+
self.node.model_def, self.model_class,
|
637
|
+
local_ctx, dynamic_outputs,
|
638
|
+
self.checkout_directory)
|
570
639
|
|
571
640
|
try:
|
572
641
|
model = self.model_class()
|
@@ -580,7 +649,10 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
|
|
580
649
|
|
581
650
|
# Check required outputs are present and build the results bundle
|
582
651
|
|
652
|
+
model_name = self.model_class.__name__
|
583
653
|
results: Bundle[_data.DataView] = dict()
|
654
|
+
new_nodes = dict()
|
655
|
+
new_deps = dict()
|
584
656
|
|
585
657
|
for output_name, output_schema in model_def.outputs.items():
|
586
658
|
|
@@ -589,7 +661,6 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
|
|
589
661
|
if result is None or result.is_empty():
|
590
662
|
|
591
663
|
if not output_schema.optional:
|
592
|
-
model_name = self.model_class.__name__
|
593
664
|
raise _ex.ERuntimeValidation(f"Missing required output [{output_name}] from model [{model_name}]")
|
594
665
|
|
595
666
|
# Create a placeholder for optional outputs that were not emitted
|
@@ -598,6 +669,30 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
|
|
598
669
|
|
599
670
|
results[output_name] = result
|
600
671
|
|
672
|
+
if dynamic_outputs:
|
673
|
+
|
674
|
+
for output_name in dynamic_outputs:
|
675
|
+
|
676
|
+
result: _data.DataView = local_ctx.get(output_name)
|
677
|
+
|
678
|
+
if result is None or result.is_empty():
|
679
|
+
raise _ex.ERuntimeValidation(f"No data provided for [{output_name}] from model [{model_name}]")
|
680
|
+
|
681
|
+
results[output_name] = result
|
682
|
+
|
683
|
+
result_node_id = NodeId.of(output_name, self.node.id.namespace, _data.DataView)
|
684
|
+
result_node = BundleItemNode(result_node_id, self.node.id, output_name)
|
685
|
+
|
686
|
+
new_nodes[result_node_id] = result_node
|
687
|
+
|
688
|
+
output_section = _graph.GraphBuilder.build_runtime_outputs(dynamic_outputs, self.node.id.namespace)
|
689
|
+
new_nodes.update(output_section.nodes)
|
690
|
+
|
691
|
+
ctx_id = NodeId.of("trac_build_result", self.node.id.namespace, result_type=None)
|
692
|
+
new_deps[ctx_id] = list(_graph.Dependency(nid, _graph.DependencyType.HARD) for nid in output_section.outputs)
|
693
|
+
|
694
|
+
self.node_callback.send_graph_updates(new_nodes, new_deps)
|
695
|
+
|
601
696
|
return results
|
602
697
|
|
603
698
|
|
@@ -621,6 +716,14 @@ class FunctionResolver:
|
|
621
716
|
:py:class:`NodeFunction <NodeFunction>`
|
622
717
|
"""
|
623
718
|
|
719
|
+
# TODO: Validate consistency for resource keys
|
720
|
+
# Storage key should be validated for load data, save data and run model with storage access
|
721
|
+
# Repository key should be validated for import model (and explicitly for run model)
|
722
|
+
|
723
|
+
# Currently jobs with missing resources will fail at runtime, with a suitable error
|
724
|
+
# The resolver is called during graph building
|
725
|
+
# Putting the check here will raise a consistency error before the job starts processing
|
726
|
+
|
624
727
|
__ResolveFunc = tp.Callable[['FunctionResolver', Node[_T]], NodeFunction[_T]]
|
625
728
|
|
626
729
|
def __init__(self, models: _models.ModelLoader, storage: _storage.StorageManager):
|
@@ -655,12 +758,13 @@ class FunctionResolver:
|
|
655
758
|
|
656
759
|
def resolve_run_model_node(self, node: RunModelNode) -> NodeFunction:
|
657
760
|
|
761
|
+
# TODO: Verify model_class against model_def
|
762
|
+
|
658
763
|
model_class = self._models.load_model_class(node.model_scope, node.model_def)
|
659
764
|
checkout_directory = self._models.model_load_checkout_directory(node.model_scope, node.model_def)
|
765
|
+
storage_manager = self._storage if node.storage_access else None
|
660
766
|
|
661
|
-
|
662
|
-
|
663
|
-
return RunModelFunc(node, model_class, checkout_directory)
|
767
|
+
return RunModelFunc(node, model_class, checkout_directory, storage_manager)
|
664
768
|
|
665
769
|
__basic_node_mapping: tp.Dict[Node.__class__, NodeFunction.__class__] = {
|
666
770
|
|
@@ -674,6 +778,7 @@ class FunctionResolver:
|
|
674
778
|
SaveJobResultNode: SaveJobResultFunc,
|
675
779
|
DataResultNode: DataResultFunc,
|
676
780
|
StaticValueNode: StaticValueFunc,
|
781
|
+
RuntimeOutputsNode: RuntimeOutputsFunc,
|
677
782
|
BundleItemNode: NoopFunc,
|
678
783
|
NoopNode: NoopFunc,
|
679
784
|
RunModelResultNode: NoopFunc
|
tracdap/rt/_exec/graph.py
CHANGED
@@ -12,8 +12,6 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
from __future__ import annotations
|
16
|
-
|
17
15
|
import pathlib
|
18
16
|
import typing as tp
|
19
17
|
import dataclasses as dc
|
@@ -38,7 +36,7 @@ class NodeNamespace:
|
|
38
36
|
return cls.__ROOT
|
39
37
|
|
40
38
|
name: str
|
41
|
-
parent: tp.Optional[NodeNamespace] = dc.field(default_factory=lambda: NodeNamespace.root())
|
39
|
+
parent: "tp.Optional[NodeNamespace]" = dc.field(default_factory=lambda: NodeNamespace.root())
|
42
40
|
|
43
41
|
def __str__(self):
|
44
42
|
if self is self.__ROOT:
|
@@ -62,7 +60,7 @@ class NodeNamespace:
|
|
62
60
|
class NodeId(tp.Generic[_T]):
|
63
61
|
|
64
62
|
@staticmethod
|
65
|
-
def of(name: str, namespace: NodeNamespace, result_type: tp.Type[_T]) -> NodeId[_T]:
|
63
|
+
def of(name: str, namespace: NodeNamespace, result_type: tp.Type[_T]) -> "NodeId[_T]":
|
66
64
|
return NodeId(name, namespace, result_type)
|
67
65
|
|
68
66
|
name: str
|
@@ -83,8 +81,8 @@ class DependencyType:
|
|
83
81
|
immediate: bool = True
|
84
82
|
tolerant: bool = False
|
85
83
|
|
86
|
-
HARD: tp.ClassVar[DependencyType]
|
87
|
-
TOLERANT: tp.ClassVar[DependencyType]
|
84
|
+
HARD: "tp.ClassVar[DependencyType]"
|
85
|
+
TOLERANT: "tp.ClassVar[DependencyType]"
|
88
86
|
|
89
87
|
|
90
88
|
DependencyType.HARD = DependencyType(immediate=True, tolerant=False)
|
@@ -93,6 +91,13 @@ DependencyType.TOLERANT = DependencyType(immediate=True, tolerant=True)
|
|
93
91
|
DependencyType.DELAYED = DependencyType(immediate=False, tolerant=False)
|
94
92
|
|
95
93
|
|
94
|
+
@dc.dataclass(frozen=True)
|
95
|
+
class Dependency:
|
96
|
+
|
97
|
+
node_id: NodeId
|
98
|
+
dependency_type: DependencyType
|
99
|
+
|
100
|
+
|
96
101
|
@dc.dataclass(frozen=True)
|
97
102
|
class Node(tp.Generic[_T]):
|
98
103
|
|
@@ -165,6 +170,17 @@ class GraphSection:
|
|
165
170
|
must_run: tp.List[NodeId] = dc.field(default_factory=list)
|
166
171
|
|
167
172
|
|
173
|
+
Bundle: tp.Generic[_T] = tp.Dict[str, _T]
|
174
|
+
ObjectBundle = Bundle[meta.ObjectDefinition]
|
175
|
+
|
176
|
+
|
177
|
+
@dc.dataclass(frozen=True)
|
178
|
+
class JobOutputs:
|
179
|
+
|
180
|
+
objects: tp.Dict[str, NodeId[meta.ObjectDefinition]] = dc.field(default_factory=dict)
|
181
|
+
bundles: tp.List[NodeId[ObjectBundle]] = dc.field(default_factory=list)
|
182
|
+
|
183
|
+
|
168
184
|
# TODO: Where does this go?
|
169
185
|
@dc.dataclass(frozen=True)
|
170
186
|
class JobResultSpec:
|
@@ -179,10 +195,6 @@ class JobResultSpec:
|
|
179
195
|
# ----------------------------------------------------------------------------------------------------------------------
|
180
196
|
|
181
197
|
|
182
|
-
Bundle: tp.Generic[_T] = tp.Dict[str, _T]
|
183
|
-
ObjectBundle = Bundle[meta.ObjectDefinition]
|
184
|
-
|
185
|
-
|
186
198
|
@_node_type
|
187
199
|
class NoopNode(Node):
|
188
200
|
pass
|
@@ -354,6 +366,7 @@ class RunModelNode(Node[Bundle[_data.DataView]]):
|
|
354
366
|
model_def: meta.ModelDefinition
|
355
367
|
parameter_ids: tp.FrozenSet[NodeId]
|
356
368
|
input_ids: tp.FrozenSet[NodeId]
|
369
|
+
storage_access: tp.Optional[tp.List[str]] = None
|
357
370
|
|
358
371
|
def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
|
359
372
|
return {dep_id: DependencyType.HARD for dep_id in [*self.parameter_ids, *self.input_ids]}
|
@@ -368,16 +381,28 @@ class RunModelResultNode(Node[None]):
|
|
368
381
|
return {self.model_id: DependencyType.HARD}
|
369
382
|
|
370
383
|
|
384
|
+
@_node_type
|
385
|
+
class RuntimeOutputsNode(Node[JobOutputs]):
|
386
|
+
|
387
|
+
outputs: JobOutputs
|
388
|
+
|
389
|
+
def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
|
390
|
+
dep_ids = [*self.outputs.bundles, *self.outputs.objects.values()]
|
391
|
+
return {node_id: DependencyType.HARD for node_id in dep_ids}
|
392
|
+
|
393
|
+
|
371
394
|
@_node_type
|
372
395
|
class BuildJobResultNode(Node[cfg.JobResult]):
|
373
396
|
|
374
397
|
job_id: meta.TagHeader
|
375
398
|
|
376
|
-
|
377
|
-
|
399
|
+
outputs: JobOutputs
|
400
|
+
runtime_outputs: tp.Optional[NodeId[JobOutputs]] = None
|
378
401
|
|
379
402
|
def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
|
380
|
-
dep_ids = [*self.bundles, *self.objects.values()]
|
403
|
+
dep_ids = [*self.outputs.bundles, *self.outputs.objects.values()]
|
404
|
+
if self.runtime_outputs is not None:
|
405
|
+
dep_ids.append(self.runtime_outputs)
|
381
406
|
return {node_id: DependencyType.HARD for node_id in dep_ids}
|
382
407
|
|
383
408
|
|
@@ -12,8 +12,6 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
from __future__ import annotations
|
16
|
-
|
17
15
|
import tracdap.rt.config as config
|
18
16
|
import tracdap.rt.exceptions as _ex
|
19
17
|
import tracdap.rt._impl.data as _data # noqa
|
@@ -42,6 +40,9 @@ class GraphBuilder:
|
|
42
40
|
if job_config.job.jobType == meta.JobType.RUN_FLOW:
|
43
41
|
return cls.build_standard_job(job_config, result_spec, cls.build_run_flow_job)
|
44
42
|
|
43
|
+
if job_config.job.jobType in [meta.JobType.IMPORT_DATA, meta.JobType.EXPORT_DATA]:
|
44
|
+
return cls.build_standard_job(job_config, result_spec, cls.build_import_export_data_job)
|
45
|
+
|
45
46
|
raise _ex.EConfigParse(f"Job type [{job_config.job.jobType}] is not supported yet")
|
46
47
|
|
47
48
|
@classmethod
|
@@ -114,6 +115,28 @@ class GraphBuilder:
|
|
114
115
|
|
115
116
|
return cls._join_sections(main_section, result_section)
|
116
117
|
|
118
|
+
@classmethod
|
119
|
+
def build_import_export_data_job(
|
120
|
+
cls, job_config: config.JobConfig, result_spec: JobResultSpec,
|
121
|
+
job_namespace: NodeNamespace, job_push_id: NodeId) \
|
122
|
+
-> GraphSection:
|
123
|
+
|
124
|
+
# TODO: These are processed as regular calculation jobs for now
|
125
|
+
# That might be ok, but is worth reviewing
|
126
|
+
|
127
|
+
if job_config.job.jobType == meta.JobType.IMPORT_DATA:
|
128
|
+
job_def = job_config.job.importData
|
129
|
+
else:
|
130
|
+
job_def = job_config.job.exportData
|
131
|
+
|
132
|
+
target_selector = job_def.model
|
133
|
+
target_obj = _util.get_job_resource(target_selector, job_config)
|
134
|
+
target_def = target_obj.model
|
135
|
+
|
136
|
+
return cls.build_calculation_job(
|
137
|
+
job_config, result_spec, job_namespace, job_push_id,
|
138
|
+
target_selector, target_def, job_def)
|
139
|
+
|
117
140
|
@classmethod
|
118
141
|
def build_run_model_job(
|
119
142
|
cls, job_config: config.JobConfig, result_spec: JobResultSpec,
|
@@ -380,6 +403,65 @@ class GraphBuilder:
|
|
380
403
|
|
381
404
|
return GraphSection(nodes, inputs=inputs)
|
382
405
|
|
406
|
+
@classmethod
|
407
|
+
def build_runtime_outputs(cls, output_names: tp.List[str], job_namespace: NodeNamespace):
|
408
|
+
|
409
|
+
# TODO: Factor out common logic with regular job outputs (including static / dynamic)
|
410
|
+
|
411
|
+
nodes = {}
|
412
|
+
inputs = set()
|
413
|
+
outputs = list()
|
414
|
+
|
415
|
+
for output_name in output_names:
|
416
|
+
|
417
|
+
# Output data view must already exist in the namespace
|
418
|
+
data_view_id = NodeId.of(output_name, job_namespace, _data.DataView)
|
419
|
+
data_spec_id = NodeId.of(f"{output_name}:SPEC", job_namespace, _data.DataSpec)
|
420
|
+
|
421
|
+
data_key = output_name + ":DATA"
|
422
|
+
data_id = _util.new_object_id(meta.ObjectType.DATA)
|
423
|
+
storage_key = output_name + ":STORAGE"
|
424
|
+
storage_id = _util.new_object_id(meta.ObjectType.STORAGE)
|
425
|
+
|
426
|
+
data_spec_node = DynamicDataSpecNode(
|
427
|
+
data_spec_id, data_view_id,
|
428
|
+
data_id, storage_id,
|
429
|
+
prior_data_spec=None)
|
430
|
+
|
431
|
+
output_data_key = _util.object_key(data_id)
|
432
|
+
output_storage_key = _util.object_key(storage_id)
|
433
|
+
|
434
|
+
# Map one data item from each view, since outputs are single part/delta
|
435
|
+
data_item_id = NodeId(f"{output_name}:ITEM", job_namespace, _data.DataItem)
|
436
|
+
data_item_node = DataItemNode(data_item_id, data_view_id)
|
437
|
+
|
438
|
+
# Create a physical save operation for the data item
|
439
|
+
data_save_id = NodeId.of(f"{output_name}:SAVE", job_namespace, None)
|
440
|
+
data_save_node = SaveDataNode(data_save_id, data_spec_id, data_item_id)
|
441
|
+
|
442
|
+
data_result_id = NodeId.of(f"{output_name}:RESULT", job_namespace, ObjectBundle)
|
443
|
+
data_result_node = DataResultNode(
|
444
|
+
data_result_id, output_name,
|
445
|
+
data_item_id, data_spec_id, data_save_id,
|
446
|
+
output_data_key, output_storage_key)
|
447
|
+
|
448
|
+
nodes[data_spec_id] = data_spec_node
|
449
|
+
nodes[data_item_id] = data_item_node
|
450
|
+
nodes[data_save_id] = data_save_node
|
451
|
+
nodes[data_result_id] = data_result_node
|
452
|
+
|
453
|
+
# Job-level data view is an input to the save operation
|
454
|
+
inputs.add(data_view_id)
|
455
|
+
outputs.append(data_result_id)
|
456
|
+
|
457
|
+
runtime_outputs = JobOutputs(bundles=outputs)
|
458
|
+
runtime_outputs_id = NodeId.of("trac_runtime_outputs", job_namespace, JobOutputs)
|
459
|
+
runtime_outputs_node = RuntimeOutputsNode(runtime_outputs_id, runtime_outputs)
|
460
|
+
|
461
|
+
nodes[runtime_outputs_id] = runtime_outputs_node
|
462
|
+
|
463
|
+
return GraphSection(nodes, inputs=inputs, outputs={runtime_outputs_id})
|
464
|
+
|
383
465
|
@classmethod
|
384
466
|
def build_job_results(
|
385
467
|
cls, job_config: cfg.JobConfig, job_namespace: NodeNamespace, result_spec: JobResultSpec,
|
@@ -396,7 +478,8 @@ class GraphBuilder:
|
|
396
478
|
|
397
479
|
build_result_node = BuildJobResultNode(
|
398
480
|
build_result_id, job_config.jobId,
|
399
|
-
objects=objects,
|
481
|
+
outputs = JobOutputs(objects=objects),
|
482
|
+
explicit_deps=explicit_deps)
|
400
483
|
|
401
484
|
elif bundles is not None:
|
402
485
|
|
@@ -404,7 +487,8 @@ class GraphBuilder:
|
|
404
487
|
|
405
488
|
build_result_node = BuildJobResultNode(
|
406
489
|
build_result_id, job_config.jobId,
|
407
|
-
bundles=bundles,
|
490
|
+
outputs = JobOutputs(bundles=bundles),
|
491
|
+
explicit_deps=explicit_deps)
|
408
492
|
|
409
493
|
else:
|
410
494
|
raise _ex.EUnexpected()
|
@@ -459,7 +543,7 @@ class GraphBuilder:
|
|
459
543
|
-> GraphSection:
|
460
544
|
|
461
545
|
if model_or_flow.objectType == meta.ObjectType.MODEL:
|
462
|
-
return cls.build_model(namespace, model_or_flow.model, explicit_deps)
|
546
|
+
return cls.build_model(job_config, namespace, model_or_flow.model, explicit_deps)
|
463
547
|
|
464
548
|
elif model_or_flow.objectType == meta.ObjectType.FLOW:
|
465
549
|
return cls.build_flow(job_config, namespace, model_or_flow.flow)
|
@@ -469,11 +553,13 @@ class GraphBuilder:
|
|
469
553
|
|
470
554
|
@classmethod
|
471
555
|
def build_model(
|
472
|
-
cls, namespace: NodeNamespace,
|
556
|
+
cls, job_config: config.JobConfig, namespace: NodeNamespace,
|
473
557
|
model_def: meta.ModelDefinition,
|
474
558
|
explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
|
475
559
|
-> GraphSection:
|
476
560
|
|
561
|
+
cls.check_model_type(job_config, model_def)
|
562
|
+
|
477
563
|
def param_id(node_name):
|
478
564
|
return NodeId(node_name, namespace, meta.Value)
|
479
565
|
|
@@ -485,6 +571,14 @@ class GraphBuilder:
|
|
485
571
|
input_ids = set(map(data_id, model_def.inputs))
|
486
572
|
output_ids = set(map(data_id, model_def.outputs))
|
487
573
|
|
574
|
+
# Set up storage access for import / export data jobs
|
575
|
+
if job_config.job.jobType == meta.JobType.IMPORT_DATA:
|
576
|
+
storage_access = job_config.job.importData.storageAccess
|
577
|
+
elif job_config.job.jobType == meta.JobType.EXPORT_DATA:
|
578
|
+
storage_access = job_config.job.exportData.storageAccess
|
579
|
+
else:
|
580
|
+
storage_access = None
|
581
|
+
|
488
582
|
# Create the model node
|
489
583
|
# Always add the prior graph root ID as a dependency
|
490
584
|
# This is to ensure dependencies are still pulled in for models with no inputs!
|
@@ -500,7 +594,8 @@ class GraphBuilder:
|
|
500
594
|
model_node = RunModelNode(
|
501
595
|
model_id, model_scope, model_def,
|
502
596
|
frozenset(parameter_ids), frozenset(input_ids),
|
503
|
-
explicit_deps=explicit_deps, bundle=model_id.namespace
|
597
|
+
explicit_deps=explicit_deps, bundle=model_id.namespace,
|
598
|
+
storage_access=storage_access)
|
504
599
|
|
505
600
|
model_result_id = NodeId(f"{model_name}:RESULT", namespace)
|
506
601
|
model_result_node = RunModelResultNode(model_result_id, model_id)
|
@@ -637,6 +732,7 @@ class GraphBuilder:
|
|
637
732
|
|
638
733
|
# Explicit check for model compatibility - report an error now, do not try build_model()
|
639
734
|
cls.check_model_compatibility(model_selector, model_obj.model, node_name, node)
|
735
|
+
cls.check_model_type(job_config, model_obj.model)
|
640
736
|
|
641
737
|
return cls.build_model_or_flow_with_context(
|
642
738
|
job_config, namespace, node_name, model_obj,
|
@@ -647,8 +743,8 @@ class GraphBuilder:
|
|
647
743
|
|
648
744
|
@classmethod
|
649
745
|
def check_model_compatibility(
|
650
|
-
cls, model_selector: meta.TagSelector,
|
651
|
-
node_name: str, flow_node: meta.FlowNode):
|
746
|
+
cls, model_selector: meta.TagSelector,
|
747
|
+
model_def: meta.ModelDefinition, node_name: str, flow_node: meta.FlowNode):
|
652
748
|
|
653
749
|
model_params = list(sorted(model_def.parameters.keys()))
|
654
750
|
model_inputs = list(sorted(model_def.inputs.keys()))
|
@@ -662,6 +758,21 @@ class GraphBuilder:
|
|
662
758
|
model_key = _util.object_key(model_selector)
|
663
759
|
raise _ex.EJobValidation(f"Incompatible model for flow node [{node_name}] (Model: [{model_key}])")
|
664
760
|
|
761
|
+
@classmethod
|
762
|
+
def check_model_type(cls, job_config: config.JobConfig, model_def: meta.ModelDefinition):
|
763
|
+
|
764
|
+
if job_config.job.jobType == meta.JobType.IMPORT_DATA:
|
765
|
+
allowed_model_types = [meta.ModelType.DATA_IMPORT_MODEL]
|
766
|
+
elif job_config.job.jobType == meta.JobType.EXPORT_DATA:
|
767
|
+
allowed_model_types = [meta.ModelType.DATA_EXPORT_MODEL]
|
768
|
+
else:
|
769
|
+
allowed_model_types = [meta.ModelType.STANDARD_MODEL]
|
770
|
+
|
771
|
+
if model_def.modelType not in allowed_model_types:
|
772
|
+
job_type = job_config.job.jobType.name
|
773
|
+
model_type = model_def.modelType.name
|
774
|
+
raise _ex.EJobValidation(f"Job type [{job_type}] cannot use model type [{model_type}]")
|
775
|
+
|
665
776
|
@staticmethod
|
666
777
|
def build_context_push(
|
667
778
|
namespace: NodeNamespace, input_mapping: tp.Dict[str, NodeId],
|