tracdap-runtime 0.8.0rc2__py3-none-any.whl → 0.9.0b1__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/_impl/core/data.py +578 -33
- tracdap/rt/_impl/core/repos.py +7 -0
- tracdap/rt/_impl/core/storage.py +10 -3
- tracdap/rt/_impl/core/util.py +54 -11
- tracdap/rt/_impl/exec/dev_mode.py +122 -100
- tracdap/rt/_impl/exec/engine.py +178 -109
- tracdap/rt/_impl/exec/functions.py +218 -257
- tracdap/rt/_impl/exec/graph.py +140 -125
- tracdap/rt/_impl/exec/graph_builder.py +411 -449
- tracdap/rt/_impl/grpc/codec.py +4 -2
- tracdap/rt/_impl/grpc/server.py +7 -7
- tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.py +25 -18
- tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.pyi +27 -9
- tracdap/rt/_impl/grpc/tracdap/metadata/common_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/config_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/custom_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/data_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/flow_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +67 -63
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +11 -2
- tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/search_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/storage_pb2.py +11 -9
- tracdap/rt/_impl/grpc/tracdap/metadata/storage_pb2.pyi +11 -2
- tracdap/rt/_impl/grpc/tracdap/metadata/tag_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/tag_update_pb2.py +1 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/type_pb2.py +1 -1
- tracdap/rt/_impl/runtime.py +8 -0
- tracdap/rt/_plugins/repo_git.py +56 -11
- tracdap/rt/_version.py +1 -1
- tracdap/rt/config/__init__.py +6 -6
- tracdap/rt/config/common.py +5 -0
- tracdap/rt/config/job.py +13 -3
- tracdap/rt/config/result.py +8 -4
- tracdap/rt/config/runtime.py +2 -0
- tracdap/rt/metadata/__init__.py +37 -36
- tracdap/rt/metadata/job.py +2 -0
- tracdap/rt/metadata/storage.py +9 -0
- {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/METADATA +3 -1
- {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/RECORD +47 -47
- {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/WHEEL +1 -1
- {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/licenses/LICENSE +0 -0
- {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/top_level.txt +0 -0
@@ -13,9 +13,11 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
|
16
|
-
import
|
16
|
+
import itertools as _itr
|
17
|
+
import typing as _tp
|
17
18
|
|
18
|
-
import tracdap.rt.
|
19
|
+
import tracdap.rt.metadata as _meta
|
20
|
+
import tracdap.rt.config as _cfg
|
19
21
|
import tracdap.rt.exceptions as _ex
|
20
22
|
import tracdap.rt._impl.core.data as _data
|
21
23
|
import tracdap.rt._impl.core.util as _util
|
@@ -25,17 +27,25 @@ from .graph import *
|
|
25
27
|
|
26
28
|
class GraphBuilder:
|
27
29
|
|
28
|
-
__JOB_DETAILS =
|
30
|
+
__JOB_DETAILS = _tp.TypeVar(
|
29
31
|
"__JOB_DETAILS",
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
_meta.RunModelJob,
|
33
|
+
_meta.RunFlowJob,
|
34
|
+
_meta.ImportModelJob,
|
35
|
+
_meta.ImportDataJob,
|
36
|
+
_meta.ExportDataJob)
|
35
37
|
|
36
|
-
__JOB_BUILD_FUNC =
|
38
|
+
__JOB_BUILD_FUNC = _tp.Callable[[_meta.JobDefinition, NodeId], GraphSection]
|
37
39
|
|
38
|
-
|
40
|
+
@classmethod
|
41
|
+
def dynamic(cls, context: GraphContext) -> "GraphBuilder":
|
42
|
+
|
43
|
+
sys_config = _cfg.RuntimeConfig(storage=context.storage_config)
|
44
|
+
job_config = _cfg.JobConfig(context.job_id)
|
45
|
+
|
46
|
+
return GraphBuilder(sys_config, job_config)
|
47
|
+
|
48
|
+
def __init__(self, sys_config: _cfg.RuntimeConfig, job_config: _cfg.JobConfig):
|
39
49
|
|
40
50
|
self._sys_config = sys_config
|
41
51
|
self._job_config = job_config
|
@@ -43,80 +53,97 @@ class GraphBuilder:
|
|
43
53
|
self._job_key = _util.object_key(job_config.jobId)
|
44
54
|
self._job_namespace = NodeNamespace(self._job_key)
|
45
55
|
|
46
|
-
|
56
|
+
# Dictionary of object type to preallocated IDs
|
57
|
+
self._preallocated_ids = dict(
|
58
|
+
(k, list(v)) for k, v in _itr.groupby(
|
59
|
+
sorted(job_config.preallocatedIds, key=lambda oid: oid.objectType.value),
|
60
|
+
lambda oid: oid.objectType))
|
61
|
+
|
62
|
+
self._errors = list()
|
63
|
+
|
64
|
+
def unallocated_ids(self) -> _tp.Dict[_meta.ObjectType, _meta.TagHeader]:
|
65
|
+
return self._preallocated_ids
|
47
66
|
|
48
|
-
def _child_builder(self, job_id:
|
67
|
+
def _child_builder(self, job_id: _meta.TagHeader) -> "GraphBuilder":
|
49
68
|
|
50
69
|
builder = GraphBuilder(self._sys_config, self._job_config)
|
51
70
|
builder._job_key = _util.object_key(job_id)
|
52
71
|
builder._job_namespace = NodeNamespace(builder._job_key)
|
53
72
|
|
73
|
+
# Do not share preallocated IDs with the child graph
|
74
|
+
builder._preallocated_ids = dict()
|
75
|
+
|
54
76
|
return builder
|
55
77
|
|
56
|
-
def build_job(self, job_def:
|
78
|
+
def build_job(self, job_def: _meta.JobDefinition, ) -> Graph:
|
57
79
|
|
58
80
|
try:
|
59
81
|
|
60
|
-
if job_def.jobType ==
|
61
|
-
|
82
|
+
if job_def.jobType == _meta.JobType.IMPORT_MODEL:
|
83
|
+
graph = self.build_standard_job(job_def, self.build_import_model_job)
|
84
|
+
|
85
|
+
elif job_def.jobType == _meta.JobType.RUN_MODEL:
|
86
|
+
graph = self.build_standard_job(job_def, self.build_run_model_job)
|
62
87
|
|
63
|
-
|
64
|
-
|
88
|
+
elif job_def.jobType == _meta.JobType.RUN_FLOW:
|
89
|
+
graph = self.build_standard_job(job_def, self.build_run_flow_job)
|
65
90
|
|
66
|
-
|
67
|
-
|
91
|
+
elif job_def.jobType in [_meta.JobType.IMPORT_DATA, _meta.JobType.EXPORT_DATA]:
|
92
|
+
graph = self.build_standard_job(job_def, self.build_import_export_data_job)
|
68
93
|
|
69
|
-
|
70
|
-
|
94
|
+
elif job_def.jobType == _meta.JobType.JOB_GROUP:
|
95
|
+
graph = self.build_standard_job(job_def, self.build_job_group)
|
71
96
|
|
72
|
-
|
73
|
-
|
97
|
+
else:
|
98
|
+
self._error(_ex.EJobValidation(f"Job type [{job_def.jobType.name}] is not supported yet"))
|
99
|
+
raise self._error_summary()
|
74
100
|
|
75
|
-
self.
|
101
|
+
if any(self._errors):
|
102
|
+
raise self._error_summary()
|
103
|
+
else:
|
104
|
+
return graph
|
76
105
|
|
77
106
|
except Exception as e:
|
78
107
|
|
79
108
|
# If there are recorded, errors, assume unhandled exceptions are a result of those
|
80
109
|
# Only report the recorded errors, to reduce noise
|
81
110
|
if any(self._errors):
|
82
|
-
|
111
|
+
raise self._error_summary()
|
83
112
|
|
84
113
|
# If no errors are recorded, an exception here would be a bug
|
85
114
|
raise _ex.ETracInternal(f"Unexpected error preparing the job execution graph") from e
|
86
115
|
|
87
|
-
|
88
|
-
|
89
|
-
if any(self._errors):
|
116
|
+
def _error_summary(self) -> Exception:
|
90
117
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
118
|
+
if len(self._errors) == 1:
|
119
|
+
return self._errors[0]
|
120
|
+
else:
|
121
|
+
err_text = "\n".join(map(str, self._errors))
|
122
|
+
return _ex.EJobValidation("Invalid job configuration\n" + err_text)
|
96
123
|
|
97
|
-
def build_standard_job(self, job_def:
|
124
|
+
def build_standard_job(self, job_def: _meta.JobDefinition, build_func: __JOB_BUILD_FUNC):
|
98
125
|
|
99
126
|
# Set up the job context
|
100
127
|
|
101
|
-
push_id = NodeId("trac_job_push", self._job_namespace, Bundle[
|
128
|
+
push_id = NodeId("trac_job_push", self._job_namespace, Bundle[_tp.Any])
|
102
129
|
push_node = ContextPushNode(push_id, self._job_namespace)
|
103
130
|
push_section = GraphSection({push_id: push_node}, must_run=[push_id])
|
104
131
|
|
105
132
|
# Build the execution graphs for the main job and results recording
|
106
133
|
|
107
134
|
main_section = build_func(job_def, push_id)
|
108
|
-
main_result_id = NodeId.of("trac_job_result", self._job_namespace,
|
135
|
+
main_result_id = NodeId.of("trac_job_result", self._job_namespace, _cfg.JobResult)
|
109
136
|
|
110
137
|
# Clean up the job context
|
111
138
|
|
112
|
-
global_result_id = NodeId.of(self._job_key, NodeNamespace.root(),
|
139
|
+
global_result_id = NodeId.of(self._job_key, NodeNamespace.root(), _cfg.JobResult)
|
113
140
|
|
114
|
-
pop_id = NodeId("trac_job_pop", self._job_namespace, Bundle[
|
141
|
+
pop_id = NodeId("trac_job_pop", self._job_namespace, Bundle[_tp.Any])
|
115
142
|
pop_mapping = {main_result_id: global_result_id}
|
116
143
|
|
117
144
|
pop_node = ContextPopNode(
|
118
145
|
pop_id, self._job_namespace, pop_mapping,
|
119
|
-
explicit_deps=main_section.must_run,
|
146
|
+
explicit_deps=[push_id, *main_section.must_run],
|
120
147
|
bundle=NodeNamespace.root())
|
121
148
|
|
122
149
|
global_result_node = BundleItemNode(global_result_id, pop_id, self._job_key)
|
@@ -129,42 +156,41 @@ class GraphBuilder:
|
|
129
156
|
|
130
157
|
return Graph(job.nodes, global_result_id)
|
131
158
|
|
132
|
-
def build_import_model_job(self, job_def:
|
159
|
+
def build_import_model_job(self, job_def: _meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
|
133
160
|
|
134
|
-
#
|
161
|
+
# TRAC object ID for the new model
|
162
|
+
model_id = self._allocate_id(_meta.ObjectType.MODEL)
|
135
163
|
|
136
|
-
# TODO: Import model job should pre-allocate an ID, then model ID comes from job_config.resultMapping
|
137
|
-
new_model_id = _util.new_object_id(meta.ObjectType.MODEL)
|
138
|
-
new_model_key = _util.object_key(new_model_id)
|
139
|
-
|
140
|
-
model_scope = self._job_key
|
141
164
|
import_details = job_def.importModel
|
165
|
+
import_scope = self._job_key
|
142
166
|
|
143
|
-
|
144
|
-
|
167
|
+
# Graph node ID for the import operation
|
168
|
+
import_id = NodeId.of("trac_import_model", self._job_namespace, GraphOutput)
|
145
169
|
|
146
|
-
|
170
|
+
import_node = ImportModelNode(
|
171
|
+
import_id, model_id,
|
172
|
+
import_details, import_scope,
|
173
|
+
explicit_deps=[job_push_id])
|
147
174
|
|
148
|
-
|
175
|
+
main_section = GraphSection(nodes={import_id: import_node})
|
149
176
|
|
150
|
-
|
151
|
-
|
152
|
-
explicit_deps=[job_push_id, *main_section.must_run])
|
177
|
+
# RESULT will have a single (unnamed) output
|
178
|
+
result_section = self.build_job_result([import_id], explicit_deps=[job_push_id, *main_section.must_run])
|
153
179
|
|
154
180
|
return self._join_sections(main_section, result_section)
|
155
181
|
|
156
|
-
def build_import_export_data_job(self, job_def:
|
182
|
+
def build_import_export_data_job(self, job_def: _meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
|
157
183
|
|
158
184
|
# TODO: These are processed as regular calculation jobs for now
|
159
185
|
# That might be ok, but is worth reviewing
|
160
186
|
|
161
|
-
if job_def.jobType ==
|
187
|
+
if job_def.jobType == _meta.JobType.IMPORT_DATA:
|
162
188
|
job_details = job_def.importData
|
163
189
|
else:
|
164
190
|
job_details = job_def.exportData
|
165
191
|
|
166
192
|
target_selector = job_details.model
|
167
|
-
target_obj = _util.
|
193
|
+
target_obj = _util.get_job_metadata(target_selector, self._job_config)
|
168
194
|
target_def = target_obj.model
|
169
195
|
|
170
196
|
return self.build_calculation_job(
|
@@ -172,12 +198,12 @@ class GraphBuilder:
|
|
172
198
|
target_selector, target_def,
|
173
199
|
job_details)
|
174
200
|
|
175
|
-
def build_run_model_job(self, job_def:
|
201
|
+
def build_run_model_job(self, job_def: _meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
|
176
202
|
|
177
203
|
job_details = job_def.runModel
|
178
204
|
|
179
205
|
target_selector = job_details.model
|
180
|
-
target_obj = _util.
|
206
|
+
target_obj = _util.get_job_metadata(target_selector, self._job_config)
|
181
207
|
target_def = target_obj.model
|
182
208
|
|
183
209
|
return self.build_calculation_job(
|
@@ -185,12 +211,12 @@ class GraphBuilder:
|
|
185
211
|
target_selector, target_def,
|
186
212
|
job_details)
|
187
213
|
|
188
|
-
def build_run_flow_job(self, job_def:
|
214
|
+
def build_run_flow_job(self, job_def: _meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
|
189
215
|
|
190
216
|
job_details = job_def.runFlow
|
191
217
|
|
192
218
|
target_selector = job_details.flow
|
193
|
-
target_obj = _util.
|
219
|
+
target_obj = _util.get_job_metadata(target_selector, self._job_config)
|
194
220
|
target_def = target_obj.flow
|
195
221
|
|
196
222
|
return self.build_calculation_job(
|
@@ -198,21 +224,21 @@ class GraphBuilder:
|
|
198
224
|
target_selector, target_def,
|
199
225
|
job_details)
|
200
226
|
|
201
|
-
def build_job_group(self, job_def:
|
227
|
+
def build_job_group(self, job_def: _meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
|
202
228
|
|
203
229
|
job_group = job_def.jobGroup
|
204
230
|
|
205
|
-
if job_group.jobGroupType ==
|
231
|
+
if job_group.jobGroupType == _meta.JobGroupType.SEQUENTIAL_JOB_GROUP:
|
206
232
|
return self.build_sequential_job_group(job_group, job_push_id)
|
207
233
|
|
208
|
-
if job_group.jobGroupType ==
|
234
|
+
if job_group.jobGroupType == _meta.JobGroupType.PARALLEL_JOB_GROUP:
|
209
235
|
return self.build_parallel_job_group(job_group, job_push_id)
|
210
236
|
|
211
237
|
else:
|
212
238
|
self._error(_ex.EJobValidation(f"Job group type [{job_group.jobGroupType.name}] is not supported yet"))
|
213
239
|
return GraphSection(dict(), inputs={job_push_id})
|
214
240
|
|
215
|
-
def build_sequential_job_group(self, job_group:
|
241
|
+
def build_sequential_job_group(self, job_group: _meta.JobGroup, job_push_id: NodeId) -> GraphSection:
|
216
242
|
|
217
243
|
nodes = dict()
|
218
244
|
prior_id = job_push_id
|
@@ -225,14 +251,14 @@ class GraphBuilder:
|
|
225
251
|
prior_id = child_node.id
|
226
252
|
|
227
253
|
# No real results from job groups yet (they cannot be executed from the platform)
|
228
|
-
job_result =
|
229
|
-
result_id = NodeId.of("trac_job_result", self._job_namespace,
|
254
|
+
job_result = _cfg.JobResult()
|
255
|
+
result_id = NodeId.of("trac_job_result", self._job_namespace, _cfg.JobResult)
|
230
256
|
result_node = StaticValueNode(result_id, job_result, explicit_deps=[prior_id])
|
231
257
|
nodes[result_id] = result_node
|
232
258
|
|
233
259
|
return GraphSection(nodes, inputs={job_push_id}, outputs={result_id})
|
234
260
|
|
235
|
-
def build_parallel_job_group(self, job_group:
|
261
|
+
def build_parallel_job_group(self, job_group: _meta.JobGroup, job_push_id: NodeId) -> GraphSection:
|
236
262
|
|
237
263
|
nodes = dict()
|
238
264
|
parallel_ids = [job_push_id]
|
@@ -245,22 +271,22 @@ class GraphBuilder:
|
|
245
271
|
parallel_ids.append(child_node.id)
|
246
272
|
|
247
273
|
# No real results from job groups yet (they cannot be executed from the platform)
|
248
|
-
job_result =
|
249
|
-
result_id = NodeId.of("trac_job_result", self._job_namespace,
|
274
|
+
job_result = _cfg.JobResult()
|
275
|
+
result_id = NodeId.of("trac_job_result", self._job_namespace, _cfg.JobResult)
|
250
276
|
result_node = StaticValueNode(result_id, job_result, explicit_deps=parallel_ids)
|
251
277
|
nodes[result_id] = result_node
|
252
278
|
|
253
279
|
return GraphSection(nodes, inputs={job_push_id}, outputs={result_id})
|
254
280
|
|
255
|
-
def build_child_job(self, child_job_def:
|
281
|
+
def build_child_job(self, child_job_def: _meta.JobDefinition, explicit_deps) -> Node[_cfg.JobResult]:
|
256
282
|
|
257
|
-
child_job_id =
|
283
|
+
child_job_id = self._allocate_id(_meta.ObjectType.JOB)
|
258
284
|
|
259
285
|
child_builder = self._child_builder(child_job_id)
|
260
286
|
child_graph = child_builder.build_job(child_job_def)
|
261
287
|
|
262
288
|
child_node_name = _util.object_key(child_job_id)
|
263
|
-
child_node_id = NodeId.of(child_node_name, self._job_namespace,
|
289
|
+
child_node_id = NodeId.of(child_node_name, self._job_namespace, _cfg.JobResult)
|
264
290
|
|
265
291
|
child_node = ChildJobNode(
|
266
292
|
child_node_id, child_job_id, child_job_def,
|
@@ -269,9 +295,9 @@ class GraphBuilder:
|
|
269
295
|
return child_node
|
270
296
|
|
271
297
|
def build_calculation_job(
|
272
|
-
self, job_def:
|
273
|
-
target_selector:
|
274
|
-
target_def:
|
298
|
+
self, job_def: _meta.JobDefinition, job_push_id: NodeId,
|
299
|
+
target_selector: _meta.TagSelector,
|
300
|
+
target_def: _tp.Union[_meta.ModelDefinition, _meta.FlowDefinition],
|
275
301
|
job_details: __JOB_DETAILS) \
|
276
302
|
-> GraphSection:
|
277
303
|
|
@@ -282,11 +308,11 @@ class GraphBuilder:
|
|
282
308
|
|
283
309
|
required_params = target_def.parameters
|
284
310
|
required_inputs = target_def.inputs
|
285
|
-
|
311
|
+
expected_outputs = target_def.outputs
|
286
312
|
|
287
313
|
provided_params = job_details.parameters
|
288
314
|
provided_inputs = job_details.inputs
|
289
|
-
|
315
|
+
prior_outputs = job_details.priorOutputs
|
290
316
|
|
291
317
|
params_section = self.build_job_parameters(
|
292
318
|
required_params, provided_params,
|
@@ -296,36 +322,48 @@ class GraphBuilder:
|
|
296
322
|
required_inputs, provided_inputs,
|
297
323
|
explicit_deps=[job_push_id])
|
298
324
|
|
325
|
+
prior_outputs_section = self.build_job_prior_outputs(
|
326
|
+
expected_outputs, prior_outputs,
|
327
|
+
explicit_deps=[job_push_id])
|
328
|
+
|
299
329
|
exec_namespace = self._job_namespace
|
300
|
-
exec_obj = _util.
|
330
|
+
exec_obj = _util.get_job_metadata(target_selector, self._job_config)
|
301
331
|
|
302
332
|
exec_section = self.build_model_or_flow(
|
303
333
|
exec_namespace, job_def, exec_obj,
|
304
334
|
explicit_deps=[job_push_id])
|
305
335
|
|
306
336
|
output_section = self.build_job_outputs(
|
307
|
-
|
337
|
+
expected_outputs, prior_outputs,
|
308
338
|
explicit_deps=[job_push_id])
|
309
339
|
|
310
|
-
main_section = self._join_sections(
|
340
|
+
main_section = self._join_sections(
|
341
|
+
params_section, input_section, prior_outputs_section,
|
342
|
+
exec_section, output_section)
|
311
343
|
|
312
344
|
# Build job-level metadata outputs
|
313
345
|
|
314
|
-
|
346
|
+
output_ids = list(
|
315
347
|
nid for nid, n in main_section.nodes.items()
|
316
|
-
if isinstance(n,
|
348
|
+
if nid.result_type == GraphOutput or isinstance(n, SaveDataNode))
|
349
|
+
|
350
|
+
# Map the SAVE nodes to their corresponding named output keys
|
351
|
+
output_keys = dict(
|
352
|
+
(nid, nid.name.replace(":SAVE", ""))
|
353
|
+
for nid, n in output_section.nodes.items()
|
354
|
+
if isinstance(n, SaveDataNode))
|
317
355
|
|
318
|
-
result_section = self.
|
319
|
-
|
356
|
+
result_section = self.build_job_result(
|
357
|
+
output_ids, output_keys,
|
320
358
|
explicit_deps=[job_push_id, *main_section.must_run])
|
321
359
|
|
322
360
|
return self._join_sections(main_section, result_section)
|
323
361
|
|
324
362
|
def build_job_parameters(
|
325
363
|
self,
|
326
|
-
required_params:
|
327
|
-
supplied_params:
|
328
|
-
explicit_deps:
|
364
|
+
required_params: _tp.Dict[str, _meta.ModelParameter],
|
365
|
+
supplied_params: _tp.Dict[str, _meta.Value],
|
366
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
329
367
|
-> GraphSection:
|
330
368
|
|
331
369
|
nodes = dict()
|
@@ -341,7 +379,7 @@ class GraphBuilder:
|
|
341
379
|
self._error(_ex.EJobValidation(f"Missing required parameter: [{param_name}]"))
|
342
380
|
continue
|
343
381
|
|
344
|
-
param_id = NodeId(param_name, self._job_namespace,
|
382
|
+
param_id = NodeId(param_name, self._job_namespace, _meta.Value)
|
345
383
|
param_node = StaticValueNode(param_id, param_def, explicit_deps=explicit_deps)
|
346
384
|
|
347
385
|
nodes[param_id] = param_node
|
@@ -350,402 +388,239 @@ class GraphBuilder:
|
|
350
388
|
|
351
389
|
def build_job_inputs(
|
352
390
|
self,
|
353
|
-
required_inputs:
|
354
|
-
supplied_inputs:
|
355
|
-
explicit_deps:
|
391
|
+
required_inputs: _tp.Dict[str, _meta.ModelInputSchema],
|
392
|
+
supplied_inputs: _tp.Dict[str, _meta.TagSelector],
|
393
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
356
394
|
-> GraphSection:
|
357
395
|
|
358
396
|
nodes = dict()
|
359
397
|
outputs = set()
|
360
398
|
|
361
|
-
for input_name,
|
362
|
-
|
363
|
-
# Backwards compatibility with pre 0.8 versions
|
364
|
-
input_type = meta.ObjectType.DATA \
|
365
|
-
if input_def.objectType == meta.ObjectType.OBJECT_TYPE_NOT_SET \
|
366
|
-
else input_def.objectType
|
399
|
+
for input_name, input_schema in required_inputs.items():
|
367
400
|
|
368
401
|
input_selector = supplied_inputs.get(input_name)
|
369
402
|
|
370
403
|
if input_selector is None:
|
371
404
|
|
372
|
-
if
|
405
|
+
if input_schema.optional:
|
373
406
|
data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
374
|
-
data_view = _data.DataView.create_empty(
|
407
|
+
data_view = _data.DataView.create_empty(input_schema.objectType)
|
375
408
|
nodes[data_view_id] = StaticValueNode(data_view_id, data_view, explicit_deps=explicit_deps)
|
376
409
|
outputs.add(data_view_id)
|
377
410
|
else:
|
378
411
|
self._error(_ex.EJobValidation(f"Missing required input: [{input_name}]"))
|
379
412
|
|
380
|
-
|
381
|
-
self._build_data_input(input_name, input_selector, nodes, outputs, explicit_deps)
|
413
|
+
continue
|
382
414
|
|
383
|
-
|
415
|
+
if input_schema.objectType == _meta.ObjectType.DATA:
|
416
|
+
self._build_data_input(input_name, input_selector, nodes, outputs, explicit_deps)
|
417
|
+
elif input_schema.objectType == _meta.ObjectType.FILE:
|
384
418
|
self._build_file_input(input_name, input_selector, nodes, outputs, explicit_deps)
|
385
|
-
|
386
419
|
else:
|
387
|
-
self._error(_ex.EJobValidation(f"Invalid input type [{
|
420
|
+
self._error(_ex.EJobValidation(f"Invalid input type [{input_schema.objectType}] for input [{input_name}]"))
|
388
421
|
|
389
422
|
return GraphSection(nodes, outputs=outputs)
|
390
423
|
|
391
|
-
def
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
if data_def.schemaId:
|
399
|
-
schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
|
400
|
-
else:
|
401
|
-
schema_def = data_def.schema
|
424
|
+
def build_job_prior_outputs(
|
425
|
+
self,
|
426
|
+
expected_outputs: _tp.Dict[str, _meta.ModelOutputSchema],
|
427
|
+
prior_outputs: _tp.Dict[str, _meta.TagSelector],
|
428
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
429
|
+
-> GraphSection:
|
402
430
|
|
403
|
-
|
404
|
-
|
405
|
-
data_spec = _data.DataSpec.create_data_spec(data_item, data_def, storage_def, schema_def)
|
431
|
+
nodes = dict()
|
432
|
+
outputs = set()
|
406
433
|
|
407
|
-
|
408
|
-
# Currently one item per input, since inputs are single part/delta
|
409
|
-
data_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
|
410
|
-
nodes[data_load_id] = LoadDataNode(data_load_id, spec=data_spec, explicit_deps=explicit_deps)
|
434
|
+
for output_name, output_schema in expected_outputs.items():
|
411
435
|
|
412
|
-
|
413
|
-
data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
414
|
-
nodes[data_view_id] = DataViewNode(data_view_id, schema_def, data_load_id)
|
415
|
-
outputs.add(data_view_id)
|
436
|
+
prior_selector = prior_outputs.get(output_name)
|
416
437
|
|
417
|
-
|
438
|
+
# Prior outputs are always optional
|
439
|
+
if prior_selector is None:
|
440
|
+
continue
|
418
441
|
|
419
|
-
|
420
|
-
|
442
|
+
if output_schema.objectType == _meta.ObjectType.DATA:
|
443
|
+
prior_spec = self._build_data_spec(prior_selector)
|
444
|
+
elif output_schema.objectType == _meta.ObjectType.FILE:
|
445
|
+
prior_spec = self._build_file_spec(prior_selector)
|
446
|
+
else:
|
447
|
+
self._error(_ex.EJobValidation(f"Invalid output type [{output_schema.objectType}] for output [{output_name}]"))
|
448
|
+
continue
|
421
449
|
|
422
|
-
|
423
|
-
|
424
|
-
|
450
|
+
prior_output_id = NodeId.of(f"{output_name}:PRIOR", self._job_namespace, _data.DataSpec)
|
451
|
+
nodes[prior_output_id] = StaticValueNode(prior_output_id, prior_spec, explicit_deps=explicit_deps)
|
452
|
+
outputs.add(prior_output_id)
|
425
453
|
|
426
|
-
|
427
|
-
file_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
428
|
-
nodes[file_view_id] = DataViewNode(file_view_id, None, file_load_id)
|
429
|
-
outputs.add(file_view_id)
|
454
|
+
return GraphSection(nodes, outputs=outputs)
|
430
455
|
|
431
456
|
def build_job_outputs(
|
432
457
|
self,
|
433
|
-
required_outputs:
|
434
|
-
|
435
|
-
explicit_deps:
|
458
|
+
required_outputs: _tp.Dict[str, _meta.ModelOutputSchema],
|
459
|
+
prior_outputs: _tp.Dict[str, _meta.TagSelector],
|
460
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
436
461
|
-> GraphSection:
|
437
462
|
|
438
463
|
nodes = {}
|
439
|
-
|
464
|
+
section_inputs = set()
|
440
465
|
|
441
|
-
for output_name,
|
466
|
+
for output_name, output_schema in required_outputs.items():
|
442
467
|
|
443
468
|
# Output data view must already exist in the namespace, it is an input to the save operation
|
444
469
|
data_view_id = NodeId.of(output_name, self._job_namespace, _data.DataView)
|
445
|
-
|
470
|
+
section_inputs.add(data_view_id)
|
446
471
|
|
447
|
-
#
|
448
|
-
|
449
|
-
if output_def.objectType == meta.ObjectType.OBJECT_TYPE_NOT_SET \
|
450
|
-
else output_def.objectType
|
472
|
+
# Check for prior outputs
|
473
|
+
prior_selector = prior_outputs.get(output_name)
|
451
474
|
|
452
|
-
|
475
|
+
if output_schema.objectType == _meta.ObjectType.DATA:
|
476
|
+
self._build_data_output(output_name, output_schema, data_view_id, prior_selector, nodes, explicit_deps)
|
477
|
+
elif output_schema.objectType == _meta.ObjectType.FILE:
|
478
|
+
self._build_file_output(output_name, output_schema, data_view_id, prior_selector, nodes, explicit_deps)
|
479
|
+
else:
|
480
|
+
self._error(_ex.EJobValidation(f"Invalid output type [{output_schema.objectType}] for input [{output_name}]"))
|
453
481
|
|
454
|
-
|
455
|
-
if output_def.optional:
|
456
|
-
optional_info = "(configuration is required for all optional outputs, in case they are produced)"
|
457
|
-
self._error(_ex.EJobValidation(f"Missing optional output: [{output_name}] {optional_info}"))
|
458
|
-
continue
|
459
|
-
else:
|
460
|
-
self._error(_ex.EJobValidation(f"Missing required output: [{output_name}]"))
|
461
|
-
continue
|
482
|
+
return GraphSection(nodes, inputs=section_inputs)
|
462
483
|
|
463
|
-
|
464
|
-
self._build_data_output(output_name, output_selector, data_view_id, nodes, explicit_deps)
|
484
|
+
def _build_data_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
|
465
485
|
|
466
|
-
|
467
|
-
self._build_file_output(output_name, output_def, output_selector, data_view_id, nodes, explicit_deps)
|
486
|
+
data_spec = self._build_data_spec(input_selector)
|
468
487
|
|
469
|
-
|
470
|
-
|
488
|
+
# Physical load of data items from disk
|
489
|
+
# Currently one item per input, since inputs are single part/delta
|
490
|
+
data_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
|
491
|
+
nodes[data_load_id] = LoadDataNode(data_load_id, spec=data_spec, explicit_deps=explicit_deps)
|
471
492
|
|
472
|
-
|
493
|
+
# Input views assembled by mapping one root part to each view
|
494
|
+
data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
495
|
+
nodes[data_view_id] = DataViewNode(data_view_id, data_spec.schema, data_load_id)
|
496
|
+
outputs.add(data_view_id)
|
473
497
|
|
474
|
-
def _build_data_output(self, output_name,
|
498
|
+
def _build_data_output(self, output_name, output_schema, data_view_id, prior_selector, nodes, explicit_deps):
|
475
499
|
|
476
500
|
# Map one data item from each view, since outputs are single part/delta
|
477
501
|
data_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
|
478
502
|
nodes[data_item_id] = DataItemNode(data_item_id, data_view_id)
|
479
503
|
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
data_def = data_obj.data
|
487
|
-
storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
|
488
|
-
|
489
|
-
if data_def.schemaId:
|
490
|
-
schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
|
491
|
-
else:
|
492
|
-
schema_def = data_def.schema
|
493
|
-
|
494
|
-
root_part_opaque_key = 'part-root' # TODO: Central part names / constants
|
495
|
-
data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
|
496
|
-
data_spec = _data.DataSpec.create_data_spec(data_item, data_def, storage_def, schema_def)
|
497
|
-
|
498
|
-
# Create a physical save operation for the data item
|
499
|
-
data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
|
500
|
-
nodes[data_save_id] = SaveDataNode(data_save_id, data_item_id, spec=data_spec)
|
501
|
-
|
502
|
-
output_key = output_name
|
503
|
-
storage_key = output_name + ":STORAGE"
|
504
|
-
|
504
|
+
if prior_selector is None:
|
505
|
+
# New output - Allocate new TRAC object IDs
|
506
|
+
prior_spec = None
|
507
|
+
data_id = self._allocate_id(_meta.ObjectType.DATA)
|
508
|
+
storage_id = self._allocate_id(_meta.ObjectType.STORAGE)
|
505
509
|
else:
|
510
|
+
# New version - Get the prior version metadata and bump the object IDs
|
511
|
+
prior_spec = self._build_data_spec(prior_selector)
|
512
|
+
data_id = _util.new_object_version(prior_spec.primary_id)
|
513
|
+
storage_id = _util.new_object_version(prior_spec.storage_id)
|
506
514
|
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
mapped_output_key = output_name
|
511
|
-
mapped_storage_key = output_name + ":STORAGE"
|
515
|
+
# Graph node ID for the save operation
|
516
|
+
data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
|
512
517
|
|
513
|
-
|
514
|
-
storage_id = self._job_config.resultMapping[mapped_storage_key]
|
518
|
+
if output_schema.dynamic:
|
515
519
|
|
516
|
-
|
517
|
-
|
520
|
+
# For dynamic outputs, an extra graph node is needed to assemble the schema information
|
521
|
+
# This will call build_data_spec() at runtime, once the schema is known
|
522
|
+
data_spec_id = NodeId.of(f"{output_name}:DYNAMIC_SCHEMA", self._job_namespace, _data.DataSpec)
|
523
|
+
nodes[data_spec_id] = DataSpecNode(
|
518
524
|
data_spec_id, data_view_id,
|
519
|
-
data_id, storage_id,
|
520
|
-
|
525
|
+
data_id, storage_id, output_name,
|
526
|
+
self._sys_config.storage,
|
527
|
+
prior_data_spec=prior_spec,
|
521
528
|
explicit_deps=explicit_deps)
|
522
529
|
|
523
|
-
#
|
524
|
-
data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
|
530
|
+
# Save operation uses the dynamically produced schema info
|
525
531
|
nodes[data_save_id] = SaveDataNode(data_save_id, data_item_id, spec_id=data_spec_id)
|
526
532
|
|
527
|
-
|
528
|
-
storage_key = _util.object_key(storage_id)
|
529
|
-
|
530
|
-
data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
|
531
|
-
nodes[data_result_id] = DataResultNode(
|
532
|
-
data_result_id, output_name, data_save_id,
|
533
|
-
data_key=output_key,
|
534
|
-
storage_key=storage_key)
|
535
|
-
|
536
|
-
def _build_file_output(self, output_name, output_def, output_selector, file_view_id, nodes, explicit_deps):
|
533
|
+
else:
|
537
534
|
|
538
|
-
|
539
|
-
|
535
|
+
# If the output is not dynamic, a data spec can be built ahead of time
|
536
|
+
data_spec = _data.build_data_spec(
|
537
|
+
data_id, storage_id, output_name,
|
538
|
+
output_schema.schema,
|
539
|
+
self._sys_config.storage,
|
540
|
+
prior_spec=prior_spec)
|
540
541
|
|
541
|
-
|
542
|
+
# Save operation uses the statically produced schema info
|
543
|
+
nodes[data_save_id] = SaveDataNode(data_save_id, data_item_id, spec=data_spec)
|
542
544
|
|
543
|
-
|
545
|
+
def _build_data_spec(self, data_selector):
|
544
546
|
|
545
|
-
|
547
|
+
# Build a data spec using metadata from the job config
|
548
|
+
# For now we are always loading the root part, snap 0, delta 0
|
549
|
+
data_def = _util.get_job_metadata(data_selector, self._job_config).data
|
550
|
+
storage_def = _util.get_job_metadata(data_def.storageId, self._job_config).storage
|
546
551
|
|
547
|
-
|
548
|
-
|
552
|
+
if data_def.schemaId:
|
553
|
+
schema_def = _util.get_job_metadata(data_def.schemaId, self._job_config).schema
|
554
|
+
else:
|
555
|
+
schema_def = data_def.schema
|
549
556
|
|
550
|
-
|
551
|
-
|
557
|
+
root_part_opaque_key = 'part-root' # TODO: Central part names / constants
|
558
|
+
data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
|
552
559
|
|
553
|
-
|
560
|
+
data_id = _util.get_job_mapping(data_selector, self._job_config)
|
561
|
+
storage_id = _util.get_job_mapping(data_def.storageId, self._job_config)
|
554
562
|
|
555
|
-
|
563
|
+
return _data.DataSpec \
|
564
|
+
.create_data_spec(data_item, data_def, storage_def, schema_def) \
|
565
|
+
.with_ids(data_id, storage_id)
|
556
566
|
|
557
|
-
|
558
|
-
storage_id = self._job_config.resultMapping[mapped_storage_key]
|
567
|
+
def _build_file_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
|
559
568
|
|
560
|
-
|
561
|
-
timestamp = _dt.datetime.fromisoformat(output_id.objectTimestamp.isoDatetime)
|
562
|
-
data_item = f"file/{output_id.objectId}/version-{output_id.objectVersion}"
|
563
|
-
storage_key = self._sys_config.storage.defaultBucket
|
564
|
-
storage_path = f"file/FILE-{output_id.objectId}/version-{output_id.objectVersion}/{output_name}.{file_type.extension}"
|
569
|
+
file_spec = self._build_file_spec(input_selector)
|
565
570
|
|
566
|
-
|
567
|
-
|
571
|
+
file_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
|
572
|
+
nodes[file_load_id] = LoadDataNode(file_load_id, spec=file_spec, explicit_deps=explicit_deps)
|
568
573
|
|
569
|
-
|
570
|
-
|
574
|
+
# Input views assembled by mapping one root part to each view
|
575
|
+
file_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
576
|
+
nodes[file_view_id] = DataViewNode(file_view_id, None, file_load_id)
|
577
|
+
outputs.add(file_view_id)
|
571
578
|
|
572
|
-
|
579
|
+
def _build_file_output(self, output_name, output_schema, file_view_id, prior_selector, nodes, explicit_deps):
|
573
580
|
|
581
|
+
# Map file item from view
|
574
582
|
file_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
|
575
583
|
nodes[file_item_id] = DataItemNode(file_item_id, file_view_id, explicit_deps=explicit_deps)
|
576
584
|
|
577
|
-
|
585
|
+
if prior_selector is None:
|
586
|
+
# New output - Allocate new TRAC object IDs
|
587
|
+
prior_spec = None
|
588
|
+
file_id = self._allocate_id(_meta.ObjectType.FILE)
|
589
|
+
storage_id = self._allocate_id(_meta.ObjectType.STORAGE)
|
590
|
+
else:
|
591
|
+
# New version - Get the prior version metadata and bump the object IDs
|
592
|
+
prior_spec = self._build_file_spec(prior_selector) if prior_selector else None
|
593
|
+
file_id = _util.new_object_version(prior_spec.primary_id)
|
594
|
+
storage_id = _util.new_object_version(prior_spec.storage_id)
|
595
|
+
|
596
|
+
# File spec can always be built ahead of time (no equivalent of dynamic schemas)
|
597
|
+
file_spec = _data.build_file_spec(
|
598
|
+
file_id, storage_id,
|
599
|
+
output_name, output_schema.fileType,
|
600
|
+
self._sys_config.storage,
|
601
|
+
prior_spec=prior_spec)
|
602
|
+
|
603
|
+
# Graph node for the save operation
|
578
604
|
file_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
|
579
605
|
nodes[file_save_id] = SaveDataNode(file_save_id, file_item_id, spec=file_spec)
|
580
606
|
|
581
|
-
|
582
|
-
nodes[data_result_id] = DataResultNode(
|
583
|
-
data_result_id, output_name, file_save_id,
|
584
|
-
file_key=resolved_output_key,
|
585
|
-
storage_key=resolved_storage_key)
|
586
|
-
|
587
|
-
@classmethod
|
588
|
-
def build_output_file_and_storage(cls, output_key, file_type: meta.FileType, sys_config: cfg.RuntimeConfig, job_config: cfg.JobConfig):
|
589
|
-
|
590
|
-
# TODO: Review and de-dupe building of output metadata
|
591
|
-
# Responsibility for assigning outputs could perhaps move from orchestrator to runtime
|
592
|
-
|
593
|
-
output_storage_key = f"{output_key}:STORAGE"
|
594
|
-
|
595
|
-
output_id = job_config.resultMapping[output_key]
|
596
|
-
output_storage_id = job_config.resultMapping[output_storage_key]
|
597
|
-
|
598
|
-
timestamp = _dt.datetime.fromisoformat(output_id.objectTimestamp.isoDatetime)
|
599
|
-
data_item = f"file/{output_id.objectId}/version-{output_id.objectVersion}"
|
600
|
-
storage_key = sys_config.storage.defaultBucket
|
601
|
-
storage_path = f"file/FILE-{output_id.objectId}/version-{output_id.objectVersion}/{output_key}.{file_type.extension}"
|
602
|
-
|
603
|
-
file_def = cls.build_file_def(output_key, file_type, output_storage_id, data_item)
|
604
|
-
storage_def = cls.build_storage_def(data_item, storage_key, storage_path, file_type.mimeType, timestamp)
|
605
|
-
|
606
|
-
return file_def, storage_def
|
607
|
-
|
608
|
-
@classmethod
|
609
|
-
def build_runtime_outputs(cls, output_names: tp.List[str], job_namespace: NodeNamespace):
|
610
|
-
|
611
|
-
# This method is called dynamically during job execution
|
612
|
-
# So it cannot use stateful information like self._job_config or self._job_namespace
|
613
|
-
|
614
|
-
# TODO: Factor out common logic with regular job outputs (including static / dynamic)
|
615
|
-
|
616
|
-
nodes = {}
|
617
|
-
inputs = set()
|
618
|
-
outputs = list()
|
619
|
-
|
620
|
-
for output_name in output_names:
|
621
|
-
|
622
|
-
# Output data view must already exist in the namespace
|
623
|
-
data_view_id = NodeId.of(output_name, job_namespace, _data.DataView)
|
624
|
-
data_spec_id = NodeId.of(f"{output_name}:SPEC", job_namespace, _data.DataSpec)
|
625
|
-
|
626
|
-
mapped_output_key = output_name
|
627
|
-
mapped_storage_key = output_name + ":STORAGE"
|
628
|
-
|
629
|
-
data_id = _util.new_object_id(meta.ObjectType.DATA)
|
630
|
-
storage_id = _util.new_object_id(meta.ObjectType.STORAGE)
|
631
|
-
|
632
|
-
data_spec_node = DynamicDataSpecNode(
|
633
|
-
data_spec_id, data_view_id,
|
634
|
-
data_id, storage_id,
|
635
|
-
prior_data_spec=None)
|
636
|
-
|
637
|
-
output_key = _util.object_key(data_id)
|
638
|
-
storage_key = _util.object_key(storage_id)
|
639
|
-
|
640
|
-
# Map one data item from each view, since outputs are single part/delta
|
641
|
-
data_item_id = NodeId(f"{output_name}:ITEM", job_namespace, _data.DataItem)
|
642
|
-
data_item_node = DataItemNode(data_item_id, data_view_id)
|
643
|
-
|
644
|
-
# Create a physical save operation for the data item
|
645
|
-
data_save_id = NodeId.of(f"{output_name}:SAVE", job_namespace, _data.DataSpec)
|
646
|
-
data_save_node = SaveDataNode(data_save_id, data_item_id, spec_id=data_spec_id)
|
647
|
-
|
648
|
-
data_result_id = NodeId.of(f"{output_name}:RESULT", job_namespace, ObjectBundle)
|
649
|
-
data_result_node = DataResultNode(
|
650
|
-
data_result_id, output_name, data_save_id,
|
651
|
-
output_key, storage_key)
|
652
|
-
|
653
|
-
nodes[data_spec_id] = data_spec_node
|
654
|
-
nodes[data_item_id] = data_item_node
|
655
|
-
nodes[data_save_id] = data_save_node
|
656
|
-
nodes[data_result_id] = data_result_node
|
657
|
-
|
658
|
-
# Job-level data view is an input to the save operation
|
659
|
-
inputs.add(data_view_id)
|
660
|
-
outputs.append(data_result_id)
|
661
|
-
|
662
|
-
runtime_outputs = JobOutputs(bundles=outputs)
|
663
|
-
runtime_outputs_id = NodeId.of("trac_runtime_outputs", job_namespace, JobOutputs)
|
664
|
-
runtime_outputs_node = RuntimeOutputsNode(runtime_outputs_id, runtime_outputs)
|
665
|
-
|
666
|
-
nodes[runtime_outputs_id] = runtime_outputs_node
|
667
|
-
|
668
|
-
return GraphSection(nodes, inputs=inputs, outputs={runtime_outputs_id})
|
669
|
-
|
670
|
-
@classmethod
|
671
|
-
def build_file_def(cls, file_name, file_type, storage_id, data_item):
|
672
|
-
|
673
|
-
file_def = meta.FileDefinition()
|
674
|
-
file_def.name = f"{file_name}.{file_type.extension}"
|
675
|
-
file_def.extension = file_type.extension
|
676
|
-
file_def.mimeType = file_type.mimeType
|
677
|
-
file_def.storageId = _util.selector_for_latest(storage_id)
|
678
|
-
file_def.dataItem = data_item
|
679
|
-
file_def.size = 0
|
680
|
-
|
681
|
-
return file_def
|
682
|
-
|
683
|
-
@classmethod
|
684
|
-
def build_storage_def(
|
685
|
-
cls, data_item: str,
|
686
|
-
storage_key, storage_path, storage_format,
|
687
|
-
timestamp: _dt.datetime):
|
688
|
-
|
689
|
-
first_incarnation = 0
|
690
|
-
|
691
|
-
storage_copy = meta.StorageCopy(
|
692
|
-
storage_key, storage_path, storage_format,
|
693
|
-
copyStatus=meta.CopyStatus.COPY_AVAILABLE,
|
694
|
-
copyTimestamp=meta.DatetimeValue(timestamp.isoformat()))
|
695
|
-
|
696
|
-
storage_incarnation = meta.StorageIncarnation(
|
697
|
-
[storage_copy],
|
698
|
-
incarnationIndex=first_incarnation,
|
699
|
-
incarnationTimestamp=meta.DatetimeValue(timestamp.isoformat()),
|
700
|
-
incarnationStatus=meta.IncarnationStatus.INCARNATION_AVAILABLE)
|
607
|
+
def _build_file_spec(self, file_selector):
|
701
608
|
|
702
|
-
|
609
|
+
file_def = _util.get_job_metadata(file_selector, self._job_config).file
|
610
|
+
storage_def = _util.get_job_metadata(file_def.storageId, self._job_config).storage
|
703
611
|
|
704
|
-
|
705
|
-
|
612
|
+
file_id = _util.get_job_mapping(file_selector, self._job_config)
|
613
|
+
storage_id = _util.get_job_mapping(file_def.storageId, self._job_config)
|
706
614
|
|
707
|
-
return
|
708
|
-
|
709
|
-
|
710
|
-
self,
|
711
|
-
objects: tp.Dict[str, NodeId[meta.ObjectDefinition]] = None,
|
712
|
-
bundles: tp.List[NodeId[ObjectBundle]] = None,
|
713
|
-
explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
|
714
|
-
-> GraphSection:
|
715
|
-
|
716
|
-
result_id = self._job_config.resultMapping.get("trac_job_result")
|
717
|
-
result_node_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
|
718
|
-
|
719
|
-
if objects is not None:
|
720
|
-
|
721
|
-
results_inputs = set(objects.values())
|
722
|
-
|
723
|
-
build_result_node = BuildJobResultNode(
|
724
|
-
result_node_id, result_id, self._job_config.jobId,
|
725
|
-
outputs=JobOutputs(objects=objects),
|
726
|
-
explicit_deps=explicit_deps)
|
727
|
-
|
728
|
-
elif bundles is not None:
|
729
|
-
|
730
|
-
results_inputs = set(bundles)
|
731
|
-
|
732
|
-
build_result_node = BuildJobResultNode(
|
733
|
-
result_node_id, result_id, self._job_config.jobId,
|
734
|
-
outputs=JobOutputs(bundles=bundles),
|
735
|
-
explicit_deps=explicit_deps)
|
736
|
-
|
737
|
-
else:
|
738
|
-
raise _ex.EUnexpected()
|
739
|
-
|
740
|
-
result_nodes = {result_node_id: build_result_node}
|
741
|
-
|
742
|
-
return GraphSection(result_nodes, inputs=results_inputs, must_run=[result_node_id])
|
615
|
+
return _data.DataSpec \
|
616
|
+
.create_file_spec(file_def.dataItem, file_def, storage_def) \
|
617
|
+
.with_ids(file_id, storage_id)
|
743
618
|
|
744
619
|
def build_model_or_flow_with_context(
|
745
620
|
self, namespace: NodeNamespace, model_or_flow_name: str,
|
746
|
-
job_def:
|
747
|
-
input_mapping:
|
748
|
-
explicit_deps:
|
621
|
+
job_def: _meta.JobDefinition, model_or_flow: _meta.ObjectDefinition,
|
622
|
+
input_mapping: _tp.Dict[str, NodeId], output_mapping: _tp.Dict[str, NodeId],
|
623
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
749
624
|
-> GraphSection:
|
750
625
|
|
751
626
|
# Generate a name for a new unique sub-context
|
@@ -772,32 +647,35 @@ class GraphBuilder:
|
|
772
647
|
|
773
648
|
def build_model_or_flow(
|
774
649
|
self, namespace: NodeNamespace,
|
775
|
-
job_def:
|
776
|
-
model_or_flow:
|
777
|
-
explicit_deps:
|
650
|
+
job_def: _meta.JobDefinition,
|
651
|
+
model_or_flow: _meta.ObjectDefinition,
|
652
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
778
653
|
-> GraphSection:
|
779
654
|
|
780
|
-
if model_or_flow.objectType ==
|
655
|
+
if model_or_flow.objectType == _meta.ObjectType.MODEL:
|
781
656
|
return self.build_model(namespace, job_def, model_or_flow.model, explicit_deps)
|
782
657
|
|
783
|
-
elif model_or_flow.objectType ==
|
658
|
+
elif model_or_flow.objectType == _meta.ObjectType.FLOW:
|
784
659
|
return self.build_flow(namespace, job_def, model_or_flow.flow)
|
785
660
|
|
786
661
|
else:
|
787
662
|
message = f"Invalid job config, expected model or flow, got [{model_or_flow.objectType}]"
|
788
663
|
self._error(_ex.EJobValidation(message))
|
789
664
|
|
665
|
+
# Allow building to continue for better error reporting
|
666
|
+
return GraphSection(dict())
|
667
|
+
|
790
668
|
def build_model(
|
791
669
|
self, namespace: NodeNamespace,
|
792
|
-
job_def:
|
793
|
-
model_def:
|
794
|
-
explicit_deps:
|
670
|
+
job_def: _meta.JobDefinition,
|
671
|
+
model_def: _meta.ModelDefinition,
|
672
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
795
673
|
-> GraphSection:
|
796
674
|
|
797
675
|
self.check_model_type(job_def, model_def)
|
798
676
|
|
799
677
|
def param_id(node_name):
|
800
|
-
return NodeId(node_name, namespace,
|
678
|
+
return NodeId(node_name, namespace, _meta.Value)
|
801
679
|
|
802
680
|
def data_id(node_name):
|
803
681
|
return NodeId(node_name, namespace, _data.DataView)
|
@@ -808,9 +686,9 @@ class GraphBuilder:
|
|
808
686
|
output_ids = set(map(data_id, model_def.outputs))
|
809
687
|
|
810
688
|
# Set up storage access for import / export data jobs
|
811
|
-
if job_def.jobType ==
|
689
|
+
if job_def.jobType == _meta.JobType.IMPORT_DATA:
|
812
690
|
storage_access = job_def.importData.storageAccess
|
813
|
-
elif job_def.jobType ==
|
691
|
+
elif job_def.jobType == _meta.JobType.EXPORT_DATA:
|
814
692
|
storage_access = job_def.exportData.storageAccess
|
815
693
|
else:
|
816
694
|
storage_access = None
|
@@ -827,16 +705,19 @@ class GraphBuilder:
|
|
827
705
|
model_name = model_def.entryPoint.split(".")[-1] # TODO: Check unique model name
|
828
706
|
model_id = NodeId(model_name, namespace, Bundle[_data.DataView])
|
829
707
|
|
708
|
+
# Used to set up a dynamic builder at runtime if dynamic graph updates are needed
|
709
|
+
context = GraphContext(
|
710
|
+
self._job_config.jobId,
|
711
|
+
self._job_namespace, namespace,
|
712
|
+
self._sys_config.storage)
|
713
|
+
|
830
714
|
model_node = RunModelNode(
|
831
|
-
model_id,
|
715
|
+
model_id, model_def, model_scope,
|
832
716
|
frozenset(parameter_ids), frozenset(input_ids),
|
833
717
|
explicit_deps=explicit_deps, bundle=model_id.namespace,
|
834
|
-
storage_access=storage_access)
|
718
|
+
storage_access=storage_access, graph_context=context)
|
835
719
|
|
836
|
-
|
837
|
-
model_result_node = RunModelResultNode(model_result_id, model_id)
|
838
|
-
|
839
|
-
nodes = {model_id: model_node, model_result_id: model_result_node}
|
720
|
+
nodes = {model_id: model_node}
|
840
721
|
|
841
722
|
# Create nodes for each model output
|
842
723
|
# The model node itself outputs a bundle (dictionary of named outputs)
|
@@ -849,13 +730,13 @@ class GraphBuilder:
|
|
849
730
|
nodes[output_id] = BundleItemNode(output_id, model_id, output_id.name)
|
850
731
|
|
851
732
|
# Assemble a graph to include the model and its outputs
|
852
|
-
return GraphSection(nodes, inputs={*parameter_ids, *input_ids}, outputs=output_ids, must_run=[
|
733
|
+
return GraphSection(nodes, inputs={*parameter_ids, *input_ids}, outputs=output_ids, must_run=[model_id])
|
853
734
|
|
854
735
|
def build_flow(
|
855
736
|
self, namespace: NodeNamespace,
|
856
|
-
job_def:
|
857
|
-
flow_def:
|
858
|
-
explicit_deps:
|
737
|
+
job_def: _meta.JobDefinition,
|
738
|
+
flow_def: _meta.FlowDefinition,
|
739
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
859
740
|
-> GraphSection:
|
860
741
|
|
861
742
|
def socket_key(socket):
|
@@ -875,7 +756,7 @@ class GraphBuilder:
|
|
875
756
|
target_edges = {socket_key(edge.target): edge for edge in flow_def.edges}
|
876
757
|
|
877
758
|
# Initially parameters and inputs are reachable, everything else is not
|
878
|
-
def is_input(n): return n[1].nodeType in [
|
759
|
+
def is_input(n): return n[1].nodeType in [_meta.FlowNodeType.PARAMETER_NODE, _meta.FlowNodeType.INPUT_NODE]
|
879
760
|
reachable_nodes = dict(filter(is_input, flow_def.nodes.items()))
|
880
761
|
remaining_nodes = dict(filter(lambda n: not is_input(n), flow_def.nodes.items()))
|
881
762
|
|
@@ -892,7 +773,7 @@ class GraphBuilder:
|
|
892
773
|
|
893
774
|
graph_section = self._join_sections(graph_section, sub_section, allow_partial_inputs=True)
|
894
775
|
|
895
|
-
if node.nodeType !=
|
776
|
+
if node.nodeType != _meta.FlowNodeType.OUTPUT_NODE:
|
896
777
|
|
897
778
|
source_edges = remaining_edges_by_source.pop(node_name)
|
898
779
|
|
@@ -916,10 +797,10 @@ class GraphBuilder:
|
|
916
797
|
|
917
798
|
def build_flow_node(
|
918
799
|
self, namespace: NodeNamespace,
|
919
|
-
job_def:
|
920
|
-
target_edges:
|
921
|
-
node_name: str, node:
|
922
|
-
explicit_deps:
|
800
|
+
job_def: _meta.JobDefinition,
|
801
|
+
target_edges: _tp.Dict[_meta.FlowSocket, _meta.FlowEdge],
|
802
|
+
node_name: str, node: _meta.FlowNode,
|
803
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
923
804
|
-> GraphSection:
|
924
805
|
|
925
806
|
def socket_key(socket):
|
@@ -930,27 +811,27 @@ class GraphBuilder:
|
|
930
811
|
return NodeId(socket_name, namespace, result_type)
|
931
812
|
|
932
813
|
def edge_mapping(node_: str, socket_: str = None, result_type=None):
|
933
|
-
socket = socket_key(
|
814
|
+
socket = socket_key(_meta.FlowSocket(node_, socket_))
|
934
815
|
edge = target_edges.get(socket)
|
935
816
|
# Report missing edges as a job consistency error (this might happen sometimes in dev mode)
|
936
817
|
if edge is None:
|
937
818
|
self._error(_ex.EJobValidation(f"Inconsistent flow: Socket [{socket}] is not connected"))
|
938
819
|
return socket_id(edge.source.node, edge.source.socket, result_type)
|
939
820
|
|
940
|
-
if node.nodeType ==
|
941
|
-
return GraphSection({}, inputs={NodeId(node_name, namespace, result_type=
|
821
|
+
if node.nodeType == _meta.FlowNodeType.PARAMETER_NODE:
|
822
|
+
return GraphSection({}, inputs={NodeId(node_name, namespace, result_type=_meta.Value)})
|
942
823
|
|
943
|
-
if node.nodeType ==
|
824
|
+
if node.nodeType == _meta.FlowNodeType.INPUT_NODE:
|
944
825
|
return GraphSection({}, inputs={NodeId(node_name, namespace, result_type=_data.DataView)})
|
945
826
|
|
946
|
-
if node.nodeType ==
|
827
|
+
if node.nodeType == _meta.FlowNodeType.OUTPUT_NODE:
|
947
828
|
target_id = NodeId(node_name, namespace, result_type=_data.DataView)
|
948
829
|
source_id = edge_mapping(node_name, None, _data.DataView)
|
949
830
|
return GraphSection({target_id: IdentityNode(target_id, source_id)}, outputs={target_id})
|
950
831
|
|
951
|
-
if node.nodeType ==
|
832
|
+
if node.nodeType == _meta.FlowNodeType.MODEL_NODE:
|
952
833
|
|
953
|
-
param_mapping = {socket: edge_mapping(node_name, socket,
|
834
|
+
param_mapping = {socket: edge_mapping(node_name, socket, _meta.Value) for socket in node.parameters}
|
954
835
|
input_mapping = {socket: edge_mapping(node_name, socket, _data.DataView) for socket in node.inputs}
|
955
836
|
output_mapping = {socket: socket_id(node_name, socket, _data.DataView) for socket in node.outputs}
|
956
837
|
|
@@ -958,10 +839,10 @@ class GraphBuilder:
|
|
958
839
|
pop_mapping = output_mapping
|
959
840
|
|
960
841
|
model_selector = job_def.runFlow.models.get(node_name)
|
961
|
-
model_obj = _util.
|
842
|
+
model_obj = _util.get_job_metadata(model_selector, self._job_config)
|
962
843
|
|
963
844
|
# Missing models in the job config is a job consistency error
|
964
|
-
if model_obj is None or model_obj.objectType !=
|
845
|
+
if model_obj is None or model_obj.objectType != _meta.ObjectType.MODEL:
|
965
846
|
self._error(_ex.EJobValidation(f"No model was provided for flow node [{node_name}]"))
|
966
847
|
|
967
848
|
# Explicit check for model compatibility - report an error now, do not try build_model()
|
@@ -976,9 +857,12 @@ class GraphBuilder:
|
|
976
857
|
|
977
858
|
self._error(_ex.EJobValidation(f"Flow node [{node_name}] has invalid node type [{node.nodeType}]"))
|
978
859
|
|
860
|
+
# Allow building to continue for better error reporting
|
861
|
+
return GraphSection(dict())
|
862
|
+
|
979
863
|
def check_model_compatibility(
|
980
|
-
self, model_selector:
|
981
|
-
model_def:
|
864
|
+
self, model_selector: _meta.TagSelector,
|
865
|
+
model_def: _meta.ModelDefinition, node_name: str, flow_node: _meta.FlowNode):
|
982
866
|
|
983
867
|
model_params = list(sorted(model_def.parameters.keys()))
|
984
868
|
model_inputs = list(sorted(model_def.inputs.keys()))
|
@@ -992,14 +876,14 @@ class GraphBuilder:
|
|
992
876
|
model_key = _util.object_key(model_selector)
|
993
877
|
self._error(_ex.EJobValidation(f"Incompatible model for flow node [{node_name}] (Model: [{model_key}])"))
|
994
878
|
|
995
|
-
def check_model_type(self, job_def:
|
879
|
+
def check_model_type(self, job_def: _meta.JobDefinition, model_def: _meta.ModelDefinition):
|
996
880
|
|
997
|
-
if job_def.jobType ==
|
998
|
-
allowed_model_types = [
|
999
|
-
elif job_def.jobType ==
|
1000
|
-
allowed_model_types = [
|
881
|
+
if job_def.jobType == _meta.JobType.IMPORT_DATA:
|
882
|
+
allowed_model_types = [_meta.ModelType.DATA_IMPORT_MODEL]
|
883
|
+
elif job_def.jobType == _meta.JobType.EXPORT_DATA:
|
884
|
+
allowed_model_types = [_meta.ModelType.DATA_EXPORT_MODEL]
|
1001
885
|
else:
|
1002
|
-
allowed_model_types = [
|
886
|
+
allowed_model_types = [_meta.ModelType.STANDARD_MODEL]
|
1003
887
|
|
1004
888
|
if model_def.modelType not in allowed_model_types:
|
1005
889
|
job_type = job_def.jobType.name
|
@@ -1008,8 +892,8 @@ class GraphBuilder:
|
|
1008
892
|
|
1009
893
|
@staticmethod
|
1010
894
|
def build_context_push(
|
1011
|
-
namespace: NodeNamespace, input_mapping:
|
1012
|
-
explicit_deps:
|
895
|
+
namespace: NodeNamespace, input_mapping: _tp.Dict[str, NodeId],
|
896
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
1013
897
|
-> GraphSection:
|
1014
898
|
|
1015
899
|
"""
|
@@ -1021,7 +905,7 @@ class GraphBuilder:
|
|
1021
905
|
for input_name, outer_id
|
1022
906
|
in input_mapping.items()}
|
1023
907
|
|
1024
|
-
push_id = NodeId("trac_ctx_push", namespace, Bundle[
|
908
|
+
push_id = NodeId("trac_ctx_push", namespace, Bundle[_tp.Any])
|
1025
909
|
push_node = ContextPushNode(push_id, namespace, push_mapping, explicit_deps, bundle=push_id.namespace)
|
1026
910
|
|
1027
911
|
nodes = {push_id: push_node}
|
@@ -1038,8 +922,8 @@ class GraphBuilder:
|
|
1038
922
|
|
1039
923
|
@staticmethod
|
1040
924
|
def build_context_pop(
|
1041
|
-
namespace: NodeNamespace, output_mapping:
|
1042
|
-
explicit_deps:
|
925
|
+
namespace: NodeNamespace, output_mapping: _tp.Dict[str, NodeId],
|
926
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
1043
927
|
-> GraphSection:
|
1044
928
|
|
1045
929
|
"""
|
@@ -1051,8 +935,14 @@ class GraphBuilder:
|
|
1051
935
|
for output_name, outer_id
|
1052
936
|
in output_mapping.items()}
|
1053
937
|
|
1054
|
-
|
1055
|
-
|
938
|
+
push_id = NodeId("trac_ctx_push", namespace, Bundle[_tp.Any])
|
939
|
+
explicit_deps = [push_id, *explicit_deps] if explicit_deps else [push_id]
|
940
|
+
|
941
|
+
pop_id = NodeId("trac_ctx_pop", namespace, Bundle[_tp.Any])
|
942
|
+
pop_node = ContextPopNode(
|
943
|
+
pop_id, namespace, pop_mapping,
|
944
|
+
explicit_deps=explicit_deps,
|
945
|
+
bundle=pop_id.namespace.parent)
|
1056
946
|
|
1057
947
|
nodes = {pop_id: pop_node}
|
1058
948
|
|
@@ -1066,6 +956,78 @@ class GraphBuilder:
|
|
1066
956
|
outputs={*pop_mapping.values()},
|
1067
957
|
must_run=[pop_id])
|
1068
958
|
|
959
|
+
def build_job_result(
|
960
|
+
self, output_ids: _tp.List[NodeId[JOB_OUTPUT_TYPE]],
|
961
|
+
output_keys: _tp.Optional[_tp.Dict[NodeId, str]] = None,
|
962
|
+
explicit_deps: _tp.Optional[_tp.List[NodeId]] = None) \
|
963
|
+
-> GraphSection:
|
964
|
+
|
965
|
+
if output_keys:
|
966
|
+
named_outputs = dict((output_keys[oid], oid) for oid in filter(lambda oid: oid in output_keys, output_ids))
|
967
|
+
unnamed_outputs = list(filter(lambda oid: oid not in output_keys, output_ids))
|
968
|
+
else:
|
969
|
+
named_outputs = dict()
|
970
|
+
unnamed_outputs = output_ids
|
971
|
+
|
972
|
+
result_node_id = NodeId.of("trac_job_result", self._job_namespace, _cfg.JobResult)
|
973
|
+
result_node = JobResultNode(
|
974
|
+
result_node_id,
|
975
|
+
self._job_config.jobId,
|
976
|
+
self._job_config.resultId,
|
977
|
+
named_outputs, unnamed_outputs,
|
978
|
+
explicit_deps=explicit_deps)
|
979
|
+
|
980
|
+
result_nodes = {result_node_id: result_node}
|
981
|
+
|
982
|
+
return GraphSection(result_nodes, inputs=set(output_ids), must_run=[result_node_id])
|
983
|
+
|
984
|
+
def build_dynamic_outputs(self, source_id: NodeId, output_names: _tp.List[str]) -> GraphUpdate:
|
985
|
+
|
986
|
+
nodes = dict()
|
987
|
+
dependencies = dict()
|
988
|
+
|
989
|
+
# All dynamic outputs are DATA with dynamic schemas for now
|
990
|
+
dynamic_schema = _meta.ModelOutputSchema(
|
991
|
+
objectType=_meta.ObjectType.DATA,
|
992
|
+
schema=None, dynamic=True)
|
993
|
+
|
994
|
+
for output_name in output_names:
|
995
|
+
|
996
|
+
# Node to extract dynamic outputs from the source node (a model or flow output bundle)
|
997
|
+
output_id = NodeId.of(output_name, source_id.namespace, _data.DataView)
|
998
|
+
output_node = BundleItemNode(output_id, source_id, output_name)
|
999
|
+
nodes[output_id] = output_node
|
1000
|
+
|
1001
|
+
# All dynamic outputs are DATA for now
|
1002
|
+
self._build_data_output(output_name, dynamic_schema, output_id, prior_selector=None, nodes=nodes,
|
1003
|
+
explicit_deps=[source_id])
|
1004
|
+
|
1005
|
+
named_outputs = dict(
|
1006
|
+
(nid.name, nid) for nid, n in nodes.items()
|
1007
|
+
if nid.result_type == GraphOutput or isinstance(n, SaveDataNode))
|
1008
|
+
|
1009
|
+
dynamic_outputs_id = NodeId.of("trac_dynamic_outputs", source_id.namespace, DynamicOutputsNode)
|
1010
|
+
dynamic_outputs_node = DynamicOutputsNode(
|
1011
|
+
dynamic_outputs_id, named_outputs,
|
1012
|
+
explicit_deps=[source_id])
|
1013
|
+
|
1014
|
+
job_result_id = NodeId.of("trac_job_result", self._job_namespace, _cfg.JobResult)
|
1015
|
+
|
1016
|
+
nodes[dynamic_outputs_id] = dynamic_outputs_node
|
1017
|
+
dependencies[job_result_id] = [Dependency(dynamic_outputs_id, DependencyType.HARD)]
|
1018
|
+
|
1019
|
+
return GraphUpdate(nodes, dependencies)
|
1020
|
+
|
1021
|
+
def _allocate_id(self, object_type: _meta.ObjectType):
|
1022
|
+
|
1023
|
+
preallocated_ids = self._preallocated_ids.get(object_type)
|
1024
|
+
|
1025
|
+
if preallocated_ids:
|
1026
|
+
# Preallocated IDs have objectVersion = 0, use a new version to get objectVersion = 1
|
1027
|
+
return _util.new_object_version(preallocated_ids.pop())
|
1028
|
+
else:
|
1029
|
+
return _util.new_object_id(object_type)
|
1030
|
+
|
1069
1031
|
def _join_sections(self, *sections: GraphSection, allow_partial_inputs: bool = False):
|
1070
1032
|
|
1071
1033
|
n_sections = len(sections)
|
@@ -1097,7 +1059,7 @@ class GraphBuilder:
|
|
1097
1059
|
|
1098
1060
|
return GraphSection(nodes, inputs, last_section.outputs, must_run)
|
1099
1061
|
|
1100
|
-
def _invalid_graph_error(self, missing_dependencies:
|
1062
|
+
def _invalid_graph_error(self, missing_dependencies: _tp.Iterable[NodeId]):
|
1101
1063
|
|
1102
1064
|
missing_ids = ", ".join(map(self._missing_item_display_name, missing_dependencies))
|
1103
1065
|
message = f"The execution graph has unsatisfied dependencies: [{missing_ids}]"
|