tracdap-runtime 0.8.0rc1__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.
Files changed (56) hide show
  1. tracdap/rt/_impl/core/data.py +578 -33
  2. tracdap/rt/_impl/core/repos.py +7 -0
  3. tracdap/rt/_impl/core/storage.py +10 -3
  4. tracdap/rt/_impl/core/util.py +54 -11
  5. tracdap/rt/_impl/exec/dev_mode.py +122 -100
  6. tracdap/rt/_impl/exec/engine.py +178 -109
  7. tracdap/rt/_impl/exec/functions.py +218 -257
  8. tracdap/rt/_impl/exec/graph.py +140 -125
  9. tracdap/rt/_impl/exec/graph_builder.py +411 -449
  10. tracdap/rt/_impl/grpc/codec.py +4 -2
  11. tracdap/rt/_impl/grpc/server.py +7 -7
  12. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.py +25 -18
  13. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.pyi +27 -9
  14. tracdap/rt/_impl/grpc/tracdap/metadata/common_pb2.py +1 -1
  15. tracdap/rt/_impl/grpc/tracdap/metadata/config_pb2.py +40 -0
  16. tracdap/rt/_impl/grpc/tracdap/metadata/config_pb2.pyi +62 -0
  17. tracdap/rt/_impl/grpc/tracdap/metadata/custom_pb2.py +1 -1
  18. tracdap/rt/_impl/grpc/tracdap/metadata/data_pb2.py +1 -1
  19. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.py +1 -1
  20. tracdap/rt/_impl/grpc/tracdap/metadata/flow_pb2.py +1 -1
  21. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +67 -63
  22. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +11 -2
  23. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +1 -1
  24. tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.py +3 -3
  25. tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.pyi +4 -0
  26. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.py +8 -6
  27. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.pyi +8 -2
  28. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +18 -5
  29. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.pyi +42 -2
  30. tracdap/rt/_impl/grpc/tracdap/metadata/search_pb2.py +1 -1
  31. tracdap/rt/_impl/grpc/tracdap/metadata/storage_pb2.py +11 -9
  32. tracdap/rt/_impl/grpc/tracdap/metadata/storage_pb2.pyi +11 -2
  33. tracdap/rt/_impl/grpc/tracdap/metadata/tag_pb2.py +1 -1
  34. tracdap/rt/_impl/grpc/tracdap/metadata/tag_update_pb2.py +1 -1
  35. tracdap/rt/_impl/grpc/tracdap/metadata/type_pb2.py +1 -1
  36. tracdap/rt/_impl/runtime.py +8 -0
  37. tracdap/rt/_plugins/repo_git.py +56 -11
  38. tracdap/rt/_version.py +1 -1
  39. tracdap/rt/config/__init__.py +6 -4
  40. tracdap/rt/config/common.py +5 -0
  41. tracdap/rt/config/dynamic.py +28 -0
  42. tracdap/rt/config/job.py +13 -3
  43. tracdap/rt/config/result.py +8 -4
  44. tracdap/rt/config/runtime.py +2 -0
  45. tracdap/rt/metadata/__init__.py +37 -30
  46. tracdap/rt/metadata/config.py +95 -0
  47. tracdap/rt/metadata/job.py +2 -0
  48. tracdap/rt/metadata/object.py +6 -0
  49. tracdap/rt/metadata/object_id.py +4 -0
  50. tracdap/rt/metadata/resource.py +41 -1
  51. tracdap/rt/metadata/storage.py +9 -0
  52. {tracdap_runtime-0.8.0rc1.dist-info → tracdap_runtime-0.9.0b1.dist-info}/METADATA +5 -2
  53. {tracdap_runtime-0.8.0rc1.dist-info → tracdap_runtime-0.9.0b1.dist-info}/RECORD +56 -52
  54. {tracdap_runtime-0.8.0rc1.dist-info → tracdap_runtime-0.9.0b1.dist-info}/WHEEL +1 -1
  55. {tracdap_runtime-0.8.0rc1.dist-info → tracdap_runtime-0.9.0b1.dist-info/licenses}/LICENSE +0 -0
  56. {tracdap_runtime-0.8.0rc1.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 datetime as _dt
16
+ import itertools as _itr
17
+ import typing as _tp
17
18
 
18
- import tracdap.rt.config as config
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 = tp.TypeVar(
30
+ __JOB_DETAILS = _tp.TypeVar(
29
31
  "__JOB_DETAILS",
30
- meta.RunModelJob,
31
- meta.RunFlowJob,
32
- meta.ImportModelJob,
33
- meta.ImportDataJob,
34
- meta.ExportDataJob)
32
+ _meta.RunModelJob,
33
+ _meta.RunFlowJob,
34
+ _meta.ImportModelJob,
35
+ _meta.ImportDataJob,
36
+ _meta.ExportDataJob)
35
37
 
36
- __JOB_BUILD_FUNC = tp.Callable[[meta.JobDefinition, NodeId], GraphSection]
38
+ __JOB_BUILD_FUNC = _tp.Callable[[_meta.JobDefinition, NodeId], GraphSection]
37
39
 
38
- def __init__(self, sys_config: config.RuntimeConfig, job_config: config.JobConfig):
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
- self._errors = []
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: meta.TagHeader) -> "GraphBuilder":
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: meta.JobDefinition,) -> Graph:
78
+ def build_job(self, job_def: _meta.JobDefinition, ) -> Graph:
57
79
 
58
80
  try:
59
81
 
60
- if job_def.jobType == meta.JobType.IMPORT_MODEL:
61
- return self.build_standard_job(job_def, self.build_import_model_job)
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
- if job_def.jobType == meta.JobType.RUN_MODEL:
64
- return self.build_standard_job(job_def, self.build_run_model_job)
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
- if job_def.jobType == meta.JobType.RUN_FLOW:
67
- return self.build_standard_job(job_def, self.build_run_flow_job)
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
- if job_def.jobType in [meta.JobType.IMPORT_DATA, meta.JobType.EXPORT_DATA]:
70
- return self.build_standard_job(job_def, self.build_import_export_data_job)
94
+ elif job_def.jobType == _meta.JobType.JOB_GROUP:
95
+ graph = self.build_standard_job(job_def, self.build_job_group)
71
96
 
72
- if job_def.jobType == meta.JobType.JOB_GROUP:
73
- return self.build_standard_job(job_def, self.build_job_group)
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._error(_ex.EJobValidation(f"Job type [{job_def.jobType.name}] is not supported yet"))
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
- pass
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
- finally:
88
-
89
- if any(self._errors):
116
+ def _error_summary(self) -> Exception:
90
117
 
91
- if len(self._errors) == 1:
92
- raise self._errors[0]
93
- else:
94
- err_text = "\n".join(map(str, self._errors))
95
- raise _ex.EJobValidation("Invalid job configuration\n" + err_text)
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: meta.JobDefinition, build_func: __JOB_BUILD_FUNC):
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[tp.Any])
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, config.JobResult)
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(), config.JobResult)
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[tp.Any])
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: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
159
+ def build_import_model_job(self, job_def: _meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
133
160
 
134
- # Main section: run the model import
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
- import_id = NodeId.of("trac_import_model", self._job_namespace, meta.ObjectDefinition)
144
- import_node = ImportModelNode(import_id, model_scope, import_details, explicit_deps=[job_push_id])
167
+ # Graph node ID for the import operation
168
+ import_id = NodeId.of("trac_import_model", self._job_namespace, GraphOutput)
145
169
 
146
- main_section = GraphSection(nodes={import_id: import_node})
170
+ import_node = ImportModelNode(
171
+ import_id, model_id,
172
+ import_details, import_scope,
173
+ explicit_deps=[job_push_id])
147
174
 
148
- # Build job-level metadata outputs
175
+ main_section = GraphSection(nodes={import_id: import_node})
149
176
 
150
- result_section = self.build_job_results(
151
- objects={new_model_key: import_id},
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: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
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 == meta.JobType.IMPORT_DATA:
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.get_job_resource(target_selector, self._job_config)
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: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
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.get_job_resource(target_selector, self._job_config)
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: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
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.get_job_resource(target_selector, self._job_config)
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: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
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 == meta.JobGroupType.SEQUENTIAL_JOB_GROUP:
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 == meta.JobGroupType.PARALLEL_JOB_GROUP:
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: meta.JobGroup, job_push_id: NodeId) -> GraphSection:
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 = cfg.JobResult()
229
- result_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
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: meta.JobGroup, job_push_id: NodeId) -> GraphSection:
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 = cfg.JobResult()
249
- result_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
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: meta.JobDefinition, explicit_deps) -> Node[config.JobResult]:
281
+ def build_child_job(self, child_job_def: _meta.JobDefinition, explicit_deps) -> Node[_cfg.JobResult]:
256
282
 
257
- child_job_id = _util.new_object_id(meta.ObjectType.JOB)
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, cfg.JobResult)
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: meta.JobDefinition, job_push_id: NodeId,
273
- target_selector: meta.TagSelector,
274
- target_def: tp.Union[meta.ModelDefinition, meta.FlowDefinition],
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
- required_outputs = target_def.outputs
311
+ expected_outputs = target_def.outputs
286
312
 
287
313
  provided_params = job_details.parameters
288
314
  provided_inputs = job_details.inputs
289
- provided_outputs = job_details.outputs
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.get_job_resource(target_selector, self._job_config)
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
- required_outputs, provided_outputs,
337
+ expected_outputs, prior_outputs,
308
338
  explicit_deps=[job_push_id])
309
339
 
310
- main_section = self._join_sections(params_section, input_section, exec_section, output_section)
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
- data_result_ids = list(
346
+ output_ids = list(
315
347
  nid for nid, n in main_section.nodes.items()
316
- if isinstance(n, DataResultNode))
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.build_job_results(
319
- bundles=data_result_ids,
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: tp.Dict[str, meta.ModelParameter],
327
- supplied_params: tp.Dict[str, meta.Value],
328
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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, meta.Value)
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: tp.Dict[str, meta.ModelInputSchema],
354
- supplied_inputs: tp.Dict[str, meta.TagSelector],
355
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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, input_def in required_inputs.items():
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 input_def.optional:
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(input_type)
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
- elif input_type == meta.ObjectType.DATA:
381
- self._build_data_input(input_name, input_selector, nodes, outputs, explicit_deps)
413
+ continue
382
414
 
383
- elif input_type == meta.ObjectType.FILE:
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 [{input_type.name}] for input [{input_name}]"))
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 _build_data_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
392
-
393
- # Build a data spec using metadata from the job config
394
- # For now we are always loading the root part, snap 0, delta 0
395
- data_def = _util.get_job_resource(input_selector, self._job_config).data
396
- storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
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
- root_part_opaque_key = 'part-root' # TODO: Central part names / constants
404
- data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
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
- # Physical load of data items from disk
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
- # Input views assembled by mapping one root part to each view
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
- def _build_file_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
438
+ # Prior outputs are always optional
439
+ if prior_selector is None:
440
+ continue
418
441
 
419
- file_def = _util.get_job_resource(input_selector, self._job_config).file
420
- storage_def = _util.get_job_resource(file_def.storageId, self._job_config).storage
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
- file_spec = _data.DataSpec.create_file_spec(file_def.dataItem, file_def, storage_def)
423
- file_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
424
- nodes[file_load_id] = LoadDataNode(file_load_id, spec=file_spec, explicit_deps=explicit_deps)
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
- # Input views assembled by mapping one root part to each view
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: tp.Dict[str, meta.ModelOutputSchema],
434
- supplied_outputs: tp.Dict[str, meta.TagSelector],
435
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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
- inputs = set()
464
+ section_inputs = set()
440
465
 
441
- for output_name, output_def in required_outputs.items():
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
- inputs.add(data_view_id)
470
+ section_inputs.add(data_view_id)
446
471
 
447
- # Backwards compatibility with pre 0.8 versions
448
- output_type = meta.ObjectType.DATA \
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
- output_selector = supplied_outputs.get(output_name)
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
- if output_selector is None:
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
- elif output_type == meta.ObjectType.DATA:
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
- elif output_type == meta.ObjectType.FILE:
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
- else:
470
- self._error(_ex.EJobValidation(f"Invalid output type [{output_type.name}] for input [{output_name}]"))
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
- return GraphSection(nodes, inputs=inputs)
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, output_selector, data_view_id, nodes, explicit_deps):
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
- data_obj = _util.get_job_resource(output_selector, self._job_config, optional=True)
481
-
482
- if data_obj is not None:
483
-
484
- # If data def for the output has been built in advance, use a static data spec
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
- # If output data def for an output was not supplied in the job, create a dynamic data spec
508
- # Dynamic data def will always use an embedded schema (this is no ID for an external schema)
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
- data_id = self._job_config.resultMapping[mapped_output_key]
514
- storage_id = self._job_config.resultMapping[mapped_storage_key]
518
+ if output_schema.dynamic:
515
519
 
516
- data_spec_id = NodeId.of(f"{output_name}:SPEC", self._job_namespace, _data.DataSpec)
517
- nodes[data_spec_id] = DynamicDataSpecNode(
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
- prior_data_spec=None,
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
- # Create a physical save operation for the data item
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
- output_key = _util.object_key(data_id)
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
- mapped_output_key = output_name
539
- mapped_storage_key = output_name + ":STORAGE"
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
- file_obj = _util.get_job_resource(output_selector, self._job_config, optional=True)
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
- if file_obj is not None:
545
+ def _build_data_spec(self, data_selector):
544
546
 
545
- # Definitions already exist (generated by dev mode translator)
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
- file_def = _util.get_job_resource(output_selector, self._job_config).file
548
- storage_def = _util.get_job_resource(file_def.storageId, self._job_config).storage
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
- resolved_output_key = mapped_output_key
551
- resolved_storage_key = mapped_storage_key
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
- else:
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
- # Create new definitions (default behavior for jobs sent from the platform)
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
- output_id = self._job_config.resultMapping[mapped_output_key]
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
- file_type = output_def.fileType
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
- file_def = self.build_file_def(output_name, file_type, storage_id, data_item)
567
- storage_def = self.build_storage_def(data_item, storage_key, storage_path, file_type.mimeType, timestamp)
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
- resolved_output_key = _util.object_key(output_id)
570
- resolved_storage_key = _util.object_key(storage_id)
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
- # Required object defs are available, now build the graph nodes
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
- file_spec = _data.DataSpec.create_file_spec(file_def.dataItem, file_def, storage_def)
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
- data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
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
- storage_item = meta.StorageItem([storage_incarnation])
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
- storage_def = meta.StorageDefinition()
705
- storage_def.dataItems[data_item] = storage_item
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 storage_def
708
-
709
- def build_job_results(
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: meta.JobDefinition, model_or_flow: meta.ObjectDefinition,
747
- input_mapping: tp.Dict[str, NodeId], output_mapping: tp.Dict[str, NodeId],
748
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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: meta.JobDefinition,
776
- model_or_flow: meta.ObjectDefinition,
777
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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 == meta.ObjectType.MODEL:
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 == meta.ObjectType.FLOW:
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: meta.JobDefinition,
793
- model_def: meta.ModelDefinition,
794
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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, meta.Value)
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 == meta.JobType.IMPORT_DATA:
689
+ if job_def.jobType == _meta.JobType.IMPORT_DATA:
812
690
  storage_access = job_def.importData.storageAccess
813
- elif job_def.jobType == meta.JobType.EXPORT_DATA:
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, model_scope, model_def,
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
- model_result_id = NodeId(f"{model_name}:RESULT", namespace)
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=[model_result_id])
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: meta.JobDefinition,
857
- flow_def: meta.FlowDefinition,
858
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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 [meta.FlowNodeType.PARAMETER_NODE, meta.FlowNodeType.INPUT_NODE]
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 != meta.FlowNodeType.OUTPUT_NODE:
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: meta.JobDefinition,
920
- target_edges: tp.Dict[meta.FlowSocket, meta.FlowEdge],
921
- node_name: str, node: meta.FlowNode,
922
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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(meta.FlowSocket(node_, socket_))
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 == meta.FlowNodeType.PARAMETER_NODE:
941
- return GraphSection({}, inputs={NodeId(node_name, namespace, result_type=meta.Value)})
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 == meta.FlowNodeType.INPUT_NODE:
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 == meta.FlowNodeType.OUTPUT_NODE:
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 == meta.FlowNodeType.MODEL_NODE:
832
+ if node.nodeType == _meta.FlowNodeType.MODEL_NODE:
952
833
 
953
- param_mapping = {socket: edge_mapping(node_name, socket, meta.Value) for socket in node.parameters}
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.get_job_resource(model_selector, self._job_config)
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 != meta.ObjectType.MODEL:
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: meta.TagSelector,
981
- model_def: meta.ModelDefinition, node_name: str, flow_node: meta.FlowNode):
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: meta.JobDefinition, model_def: meta.ModelDefinition):
879
+ def check_model_type(self, job_def: _meta.JobDefinition, model_def: _meta.ModelDefinition):
996
880
 
997
- if job_def.jobType == meta.JobType.IMPORT_DATA:
998
- allowed_model_types = [meta.ModelType.DATA_IMPORT_MODEL]
999
- elif job_def.jobType == meta.JobType.EXPORT_DATA:
1000
- allowed_model_types = [meta.ModelType.DATA_EXPORT_MODEL]
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 = [meta.ModelType.STANDARD_MODEL]
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: tp.Dict[str, NodeId],
1012
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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[tp.Any])
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: tp.Dict[str, NodeId],
1042
- explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
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
- pop_id = NodeId("trac_ctx_pop", namespace, Bundle[tp.Any])
1055
- pop_node = ContextPopNode(pop_id, namespace, pop_mapping, explicit_deps, bundle=pop_id.namespace.parent)
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: tp.Iterable[NodeId]):
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}]"