tracdap-runtime 0.6.5__py3-none-any.whl → 0.7.0__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 (89) hide show
  1. tracdap/rt/__init__.py +6 -5
  2. tracdap/rt/_exec/actors.py +6 -5
  3. tracdap/rt/_exec/context.py +278 -110
  4. tracdap/rt/_exec/dev_mode.py +237 -143
  5. tracdap/rt/_exec/engine.py +223 -64
  6. tracdap/rt/_exec/functions.py +31 -6
  7. tracdap/rt/_exec/graph.py +15 -5
  8. tracdap/rt/_exec/graph_builder.py +301 -203
  9. tracdap/rt/_exec/runtime.py +13 -10
  10. tracdap/rt/_exec/server.py +6 -5
  11. tracdap/rt/_impl/__init__.py +6 -5
  12. tracdap/rt/_impl/config_parser.py +17 -9
  13. tracdap/rt/_impl/data.py +284 -172
  14. tracdap/rt/_impl/ext/__init__.py +14 -0
  15. tracdap/rt/_impl/ext/sql.py +117 -0
  16. tracdap/rt/_impl/ext/storage.py +58 -0
  17. tracdap/rt/_impl/grpc/__init__.py +6 -5
  18. tracdap/rt/_impl/grpc/codec.py +6 -5
  19. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +62 -54
  20. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +37 -2
  21. tracdap/rt/_impl/guard_rails.py +6 -5
  22. tracdap/rt/_impl/models.py +6 -5
  23. tracdap/rt/_impl/repos.py +6 -5
  24. tracdap/rt/_impl/schemas.py +6 -5
  25. tracdap/rt/_impl/shim.py +6 -5
  26. tracdap/rt/_impl/static_api.py +30 -16
  27. tracdap/rt/_impl/storage.py +8 -7
  28. tracdap/rt/_impl/type_system.py +6 -5
  29. tracdap/rt/_impl/util.py +16 -5
  30. tracdap/rt/_impl/validation.py +72 -18
  31. tracdap/rt/_plugins/__init__.py +6 -5
  32. tracdap/rt/_plugins/_helpers.py +6 -5
  33. tracdap/rt/_plugins/config_local.py +6 -5
  34. tracdap/rt/_plugins/format_arrow.py +6 -5
  35. tracdap/rt/_plugins/format_csv.py +6 -5
  36. tracdap/rt/_plugins/format_parquet.py +6 -5
  37. tracdap/rt/_plugins/repo_git.py +6 -5
  38. tracdap/rt/_plugins/repo_local.py +6 -5
  39. tracdap/rt/_plugins/repo_pypi.py +6 -5
  40. tracdap/rt/_plugins/storage_aws.py +6 -5
  41. tracdap/rt/_plugins/storage_azure.py +6 -5
  42. tracdap/rt/_plugins/storage_gcp.py +6 -5
  43. tracdap/rt/_plugins/storage_local.py +6 -5
  44. tracdap/rt/_plugins/storage_sql.py +418 -0
  45. tracdap/rt/_plugins/storage_sql_dialects.py +118 -0
  46. tracdap/rt/_version.py +7 -6
  47. tracdap/rt/api/__init__.py +23 -5
  48. tracdap/rt/api/experimental.py +85 -37
  49. tracdap/rt/api/hook.py +16 -5
  50. tracdap/rt/api/model_api.py +110 -90
  51. tracdap/rt/api/static_api.py +142 -100
  52. tracdap/rt/config/common.py +26 -27
  53. tracdap/rt/config/job.py +5 -6
  54. tracdap/rt/config/platform.py +41 -42
  55. tracdap/rt/config/result.py +5 -6
  56. tracdap/rt/config/runtime.py +6 -7
  57. tracdap/rt/exceptions.py +13 -7
  58. tracdap/rt/ext/__init__.py +6 -5
  59. tracdap/rt/ext/config.py +6 -5
  60. tracdap/rt/ext/embed.py +6 -5
  61. tracdap/rt/ext/plugins.py +6 -5
  62. tracdap/rt/ext/repos.py +6 -5
  63. tracdap/rt/ext/storage.py +6 -5
  64. tracdap/rt/launch/__init__.py +10 -5
  65. tracdap/rt/launch/__main__.py +6 -5
  66. tracdap/rt/launch/cli.py +6 -5
  67. tracdap/rt/launch/launch.py +38 -15
  68. tracdap/rt/metadata/__init__.py +4 -0
  69. tracdap/rt/metadata/common.py +2 -3
  70. tracdap/rt/metadata/custom.py +3 -4
  71. tracdap/rt/metadata/data.py +30 -31
  72. tracdap/rt/metadata/file.py +6 -7
  73. tracdap/rt/metadata/flow.py +22 -23
  74. tracdap/rt/metadata/job.py +89 -45
  75. tracdap/rt/metadata/model.py +26 -27
  76. tracdap/rt/metadata/object.py +11 -12
  77. tracdap/rt/metadata/object_id.py +23 -24
  78. tracdap/rt/metadata/resource.py +0 -1
  79. tracdap/rt/metadata/search.py +15 -16
  80. tracdap/rt/metadata/stoarge.py +22 -23
  81. tracdap/rt/metadata/tag.py +8 -9
  82. tracdap/rt/metadata/tag_update.py +11 -12
  83. tracdap/rt/metadata/type.py +38 -38
  84. {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/LICENSE +1 -1
  85. {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/METADATA +4 -2
  86. tracdap_runtime-0.7.0.dist-info/RECORD +121 -0
  87. {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/WHEEL +1 -1
  88. tracdap_runtime-0.6.5.dist-info/RECORD +0 -116
  89. {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,9 @@
1
- # Copyright 2022 Accenture Global Solutions Limited
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
1
+ # Licensed to the Fintech Open Source Foundation (FINOS) under one or
2
+ # more contributor license agreements. See the NOTICE file distributed
3
+ # with this work for additional information regarding copyright ownership.
4
+ # FINOS licenses this file to you under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the
6
+ # License. You may obtain a copy of the License at
6
7
  #
7
8
  # http://www.apache.org/licenses/LICENSE-2.0
8
9
  #
@@ -22,75 +23,111 @@ from .graph import *
22
23
 
23
24
  class GraphBuilder:
24
25
 
25
- __JOB_BUILD_FUNC = tp.Callable[
26
- [config.JobConfig, JobResultSpec, NodeNamespace, NodeId],
27
- GraphSection]
26
+ __JOB_DETAILS = tp.TypeVar(
27
+ "__JOB_DETAILS",
28
+ meta.RunModelJob,
29
+ meta.RunFlowJob,
30
+ meta.ImportModelJob,
31
+ meta.ImportDataJob,
32
+ meta.ExportDataJob)
28
33
 
29
- @classmethod
30
- def build_job(
31
- cls, job_config: config.JobConfig,
32
- result_spec: JobResultSpec) -> Graph:
34
+ __JOB_BUILD_FUNC = tp.Callable[[meta.JobDefinition, NodeId], GraphSection]
33
35
 
34
- if job_config.job.jobType == meta.JobType.IMPORT_MODEL:
35
- return cls.build_standard_job(job_config, result_spec, cls.build_import_model_job)
36
+ def __init__(self, job_config: config.JobConfig, result_spec: JobResultSpec):
36
37
 
37
- if job_config.job.jobType == meta.JobType.RUN_MODEL:
38
- return cls.build_standard_job(job_config, result_spec, cls.build_run_model_job)
38
+ self._job_config = job_config
39
+ self._result_spec = result_spec
39
40
 
40
- if job_config.job.jobType == meta.JobType.RUN_FLOW:
41
- return cls.build_standard_job(job_config, result_spec, cls.build_run_flow_job)
41
+ self._job_key = _util.object_key(job_config.jobId)
42
+ self._job_namespace = NodeNamespace(self._job_key)
42
43
 
43
- if job_config.job.jobType in [meta.JobType.IMPORT_DATA, meta.JobType.EXPORT_DATA]:
44
- return cls.build_standard_job(job_config, result_spec, cls.build_import_export_data_job)
44
+ self._errors = []
45
45
 
46
- raise _ex.EConfigParse(f"Job type [{job_config.job.jobType}] is not supported yet")
46
+ def _child_builder(self, job_id: meta.TagHeader) -> "GraphBuilder":
47
47
 
48
- @classmethod
49
- def build_standard_job(
50
- cls, job_config: config.JobConfig, result_spec: JobResultSpec,
51
- build_func: __JOB_BUILD_FUNC):
48
+ builder = GraphBuilder(self._job_config, JobResultSpec(save_result=False))
49
+ builder._job_key = _util.object_key(job_id)
50
+ builder._job_namespace = NodeNamespace(builder._job_key)
52
51
 
53
- # Set up the job context
52
+ return builder
53
+
54
+ def build_job(self, job_def: meta.JobDefinition,) -> Graph:
55
+
56
+ try:
57
+
58
+ if job_def.jobType == meta.JobType.IMPORT_MODEL:
59
+ return self.build_standard_job(job_def, self.build_import_model_job)
60
+
61
+ if job_def.jobType == meta.JobType.RUN_MODEL:
62
+ return self.build_standard_job(job_def, self.build_run_model_job)
63
+
64
+ if job_def.jobType == meta.JobType.RUN_FLOW:
65
+ return self.build_standard_job(job_def, self.build_run_flow_job)
66
+
67
+ if job_def.jobType in [meta.JobType.IMPORT_DATA, meta.JobType.EXPORT_DATA]:
68
+ return self.build_standard_job(job_def, self.build_import_export_data_job)
69
+
70
+ if job_def.jobType == meta.JobType.JOB_GROUP:
71
+ return self.build_standard_job(job_def, self.build_job_group)
54
72
 
55
- job_key = _util.object_key(job_config.jobId)
56
- job_namespace = NodeNamespace(job_key)
73
+ self._error(_ex.EJobValidation(f"Job type [{job_def.jobType.name}] is not supported yet"))
57
74
 
58
- push_id = NodeId("trac_job_push", job_namespace, Bundle[tp.Any])
59
- push_node = ContextPushNode(push_id, job_namespace)
75
+ except Exception as e:
76
+
77
+ # If there are recorded, errors, assume unhandled exceptions are a result of those
78
+ # Only report the recorded errors, to reduce noise
79
+ if any(self._errors):
80
+ pass
81
+
82
+ # If no errors are recorded, an exception here would be a bug
83
+ raise _ex.ETracInternal(f"Unexpected error preparing the job execution graph") from e
84
+
85
+ finally:
86
+
87
+ if any(self._errors):
88
+
89
+ if len(self._errors) == 1:
90
+ raise self._errors[0]
91
+ else:
92
+ err_text = "\n".join(map(str, self._errors))
93
+ raise _ex.EJobValidation("Invalid job configuration\n" + err_text)
94
+
95
+ def build_standard_job(self, job_def: meta.JobDefinition, build_func: __JOB_BUILD_FUNC):
96
+
97
+ # Set up the job context
98
+
99
+ push_id = NodeId("trac_job_push", self._job_namespace, Bundle[tp.Any])
100
+ push_node = ContextPushNode(push_id, self._job_namespace)
60
101
  push_section = GraphSection({push_id: push_node}, must_run=[push_id])
61
102
 
62
103
  # Build the execution graphs for the main job and results recording
63
104
 
64
- main_section = build_func(job_config, result_spec, job_namespace, push_id)
65
- main_result_id = NodeId.of("trac_build_result", job_namespace, config.JobResult)
105
+ main_section = build_func(job_def, push_id)
106
+ main_result_id = NodeId.of("trac_job_result", self._job_namespace, config.JobResult)
66
107
 
67
108
  # Clean up the job context
68
109
 
69
- global_result_id = NodeId.of(job_key, NodeNamespace.root(), config.JobResult)
110
+ global_result_id = NodeId.of(self._job_key, NodeNamespace.root(), config.JobResult)
70
111
 
71
- pop_id = NodeId("trac_job_pop", job_namespace, Bundle[tp.Any])
112
+ pop_id = NodeId("trac_job_pop", self._job_namespace, Bundle[tp.Any])
72
113
  pop_mapping = {main_result_id: global_result_id}
73
114
 
74
115
  pop_node = ContextPopNode(
75
- pop_id, job_namespace, pop_mapping,
116
+ pop_id, self._job_namespace, pop_mapping,
76
117
  explicit_deps=main_section.must_run,
77
118
  bundle=NodeNamespace.root())
78
119
 
79
- global_result_node = BundleItemNode(global_result_id, pop_id, job_key)
120
+ global_result_node = BundleItemNode(global_result_id, pop_id, self._job_key)
80
121
 
81
122
  pop_section = GraphSection({
82
123
  pop_id: pop_node,
83
124
  global_result_id: global_result_node})
84
125
 
85
- job = cls._join_sections(push_section, main_section, pop_section)
126
+ job = self._join_sections(push_section, main_section, pop_section)
86
127
 
87
128
  return Graph(job.nodes, global_result_id)
88
129
 
89
- @classmethod
90
- def build_import_model_job(
91
- cls, job_config: config.JobConfig, result_spec: JobResultSpec,
92
- job_namespace: NodeNamespace, job_push_id: NodeId) \
93
- -> GraphSection:
130
+ def build_import_model_job(self, job_def: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
94
131
 
95
132
  # Main section: run the model import
96
133
 
@@ -98,82 +135,142 @@ class GraphBuilder:
98
135
  new_model_id = _util.new_object_id(meta.ObjectType.MODEL)
99
136
  new_model_key = _util.object_key(new_model_id)
100
137
 
101
- model_scope = _util.object_key(job_config.jobId)
102
- import_details = job_config.job.importModel
138
+ model_scope = self._job_key
139
+ import_details = job_def.importModel
103
140
 
104
- import_id = NodeId.of("trac_import_model", job_namespace, meta.ObjectDefinition)
141
+ import_id = NodeId.of("trac_import_model", self._job_namespace, meta.ObjectDefinition)
105
142
  import_node = ImportModelNode(import_id, model_scope, import_details, explicit_deps=[job_push_id])
106
143
 
107
144
  main_section = GraphSection(nodes={import_id: import_node})
108
145
 
109
146
  # Build job-level metadata outputs
110
147
 
111
- result_section = cls.build_job_results(
112
- job_config, job_namespace, result_spec,
148
+ result_section = self.build_job_results(
113
149
  objects={new_model_key: import_id},
114
150
  explicit_deps=[job_push_id, *main_section.must_run])
115
151
 
116
- return cls._join_sections(main_section, result_section)
152
+ return self._join_sections(main_section, result_section)
117
153
 
118
- @classmethod
119
- def build_import_export_data_job(
120
- cls, job_config: config.JobConfig, result_spec: JobResultSpec,
121
- job_namespace: NodeNamespace, job_push_id: NodeId) \
122
- -> GraphSection:
154
+ def build_import_export_data_job(self, job_def: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
123
155
 
124
156
  # TODO: These are processed as regular calculation jobs for now
125
157
  # That might be ok, but is worth reviewing
126
158
 
127
- if job_config.job.jobType == meta.JobType.IMPORT_DATA:
128
- job_def = job_config.job.importData
159
+ if job_def.jobType == meta.JobType.IMPORT_DATA:
160
+ job_details = job_def.importData
129
161
  else:
130
- job_def = job_config.job.exportData
162
+ job_details = job_def.exportData
131
163
 
132
- target_selector = job_def.model
133
- target_obj = _util.get_job_resource(target_selector, job_config)
164
+ target_selector = job_details.model
165
+ target_obj = _util.get_job_resource(target_selector, self._job_config)
134
166
  target_def = target_obj.model
135
167
 
136
- return cls.build_calculation_job(
137
- job_config, result_spec, job_namespace, job_push_id,
138
- target_selector, target_def, job_def)
168
+ return self.build_calculation_job(
169
+ job_def, job_push_id,
170
+ target_selector, target_def,
171
+ job_details)
139
172
 
140
- @classmethod
141
- def build_run_model_job(
142
- cls, job_config: config.JobConfig, result_spec: JobResultSpec,
143
- job_namespace: NodeNamespace, job_push_id: NodeId) \
144
- -> GraphSection:
173
+ def build_run_model_job(self, job_def: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
174
+
175
+ job_details = job_def.runModel
145
176
 
146
- target_selector = job_config.job.runModel.model
147
- target_obj = _util.get_job_resource(target_selector, job_config)
177
+ target_selector = job_details.model
178
+ target_obj = _util.get_job_resource(target_selector, self._job_config)
148
179
  target_def = target_obj.model
149
- job_def = job_config.job.runModel
150
180
 
151
- return cls.build_calculation_job(
152
- job_config, result_spec, job_namespace, job_push_id,
153
- target_selector, target_def, job_def)
181
+ return self.build_calculation_job(
182
+ job_def, job_push_id,
183
+ target_selector, target_def,
184
+ job_details)
154
185
 
155
- @classmethod
156
- def build_run_flow_job(
157
- cls, job_config: config.JobConfig, result_spec: JobResultSpec,
158
- job_namespace: NodeNamespace, job_push_id: NodeId) \
159
- -> GraphSection:
186
+ def build_run_flow_job(self, job_def: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
187
+
188
+ job_details = job_def.runFlow
160
189
 
161
- target_selector = job_config.job.runFlow.flow
162
- target_obj = _util.get_job_resource(target_selector, job_config)
190
+ target_selector = job_details.flow
191
+ target_obj = _util.get_job_resource(target_selector, self._job_config)
163
192
  target_def = target_obj.flow
164
- job_def = job_config.job.runFlow
165
193
 
166
- return cls.build_calculation_job(
167
- job_config, result_spec, job_namespace, job_push_id,
168
- target_selector, target_def, job_def)
194
+ return self.build_calculation_job(
195
+ job_def, job_push_id,
196
+ target_selector, target_def,
197
+ job_details)
198
+
199
+ def build_job_group(self, job_def: meta.JobDefinition, job_push_id: NodeId) -> GraphSection:
200
+
201
+ job_group = job_def.jobGroup
202
+
203
+ if job_group.jobGroupType == meta.JobGroupType.SEQUENTIAL_JOB_GROUP:
204
+ return self.build_sequential_job_group(job_group, job_push_id)
205
+
206
+ if job_group.jobGroupType == meta.JobGroupType.PARALLEL_JOB_GROUP:
207
+ return self.build_parallel_job_group(job_group, job_push_id)
208
+
209
+ else:
210
+ self._error(_ex.EJobValidation(f"Job group type [{job_group.jobGroupType.name}] is not supported yet"))
211
+ return GraphSection(dict(), inputs={job_push_id})
212
+
213
+ def build_sequential_job_group(self, job_group: meta.JobGroup, job_push_id: NodeId) -> GraphSection:
214
+
215
+ nodes = dict()
216
+ prior_id = job_push_id
217
+
218
+ for child_def in job_group.sequential.jobs:
219
+
220
+ child_node = self.build_child_job(child_def, explicit_deps=[prior_id])
221
+ nodes[child_node.id] = child_node
222
+
223
+ prior_id = child_node.id
224
+
225
+ # No real results from job groups yet (they cannot be executed from the platform)
226
+ job_result = cfg.JobResult()
227
+ result_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
228
+ result_node = StaticValueNode(result_id, job_result, explicit_deps=[prior_id])
229
+ nodes[result_id] = result_node
230
+
231
+ return GraphSection(nodes, inputs={job_push_id}, outputs={result_id})
232
+
233
+ def build_parallel_job_group(self, job_group: meta.JobGroup, job_push_id: NodeId) -> GraphSection:
234
+
235
+ nodes = dict()
236
+ parallel_ids = [job_push_id]
237
+
238
+ for child_def in job_group.parallel.jobs:
239
+
240
+ child_node = self.build_child_job(child_def, explicit_deps=[job_push_id])
241
+ nodes[child_node.id] = child_node
242
+
243
+ parallel_ids.append(child_node.id)
244
+
245
+ # No real results from job groups yet (they cannot be executed from the platform)
246
+ job_result = cfg.JobResult()
247
+ result_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
248
+ result_node = StaticValueNode(result_id, job_result, explicit_deps=parallel_ids)
249
+ nodes[result_id] = result_node
250
+
251
+ return GraphSection(nodes, inputs={job_push_id}, outputs={result_id})
252
+
253
+ def build_child_job(self, child_job_def: meta.JobDefinition, explicit_deps) -> Node[config.JobResult]:
254
+
255
+ child_job_id = _util.new_object_id(meta.ObjectType.JOB)
256
+
257
+ child_builder = self._child_builder(child_job_id)
258
+ child_graph = child_builder.build_job(child_job_def)
259
+
260
+ child_node_name = _util.object_key(child_job_id)
261
+ child_node_id = NodeId.of(child_node_name, self._job_namespace, cfg.JobResult)
262
+
263
+ child_node = ChildJobNode(
264
+ child_node_id, child_job_id, child_job_def,
265
+ child_graph, explicit_deps)
266
+
267
+ return child_node
169
268
 
170
- @classmethod
171
269
  def build_calculation_job(
172
- cls, job_config: config.JobConfig, result_spec: JobResultSpec,
173
- job_namespace: NodeNamespace, job_push_id: NodeId,
270
+ self, job_def: meta.JobDefinition, job_push_id: NodeId,
174
271
  target_selector: meta.TagSelector,
175
272
  target_def: tp.Union[meta.ModelDefinition, meta.FlowDefinition],
176
- job_def: tp.Union[meta.RunModelJob, meta.RunFlowJob]) \
273
+ job_details: __JOB_DETAILS) \
177
274
  -> GraphSection:
178
275
 
179
276
  # The main execution graph can run directly in the job context, no need to do a context push
@@ -185,29 +282,30 @@ class GraphBuilder:
185
282
  required_inputs = target_def.inputs
186
283
  required_outputs = target_def.outputs
187
284
 
188
- provided_params = job_def.parameters
189
- provided_inputs = job_def.inputs
190
- provided_outputs = job_def.outputs
285
+ provided_params = job_details.parameters
286
+ provided_inputs = job_details.inputs
287
+ provided_outputs = job_details.outputs
191
288
 
192
- params_section = cls.build_job_parameters(
193
- job_namespace, required_params, provided_params,
289
+ params_section = self.build_job_parameters(
290
+ required_params, provided_params,
194
291
  explicit_deps=[job_push_id])
195
292
 
196
- input_section = cls.build_job_inputs(
197
- job_config, job_namespace, required_inputs, provided_inputs,
293
+ input_section = self.build_job_inputs(
294
+ required_inputs, provided_inputs,
198
295
  explicit_deps=[job_push_id])
199
296
 
200
- exec_obj = _util.get_job_resource(target_selector, job_config)
297
+ exec_namespace = self._job_namespace
298
+ exec_obj = _util.get_job_resource(target_selector, self._job_config)
201
299
 
202
- exec_section = cls.build_model_or_flow(
203
- job_config, job_namespace, exec_obj,
300
+ exec_section = self.build_model_or_flow(
301
+ exec_namespace, job_def, exec_obj,
204
302
  explicit_deps=[job_push_id])
205
303
 
206
- output_section = cls.build_job_outputs(
207
- job_config, job_namespace, required_outputs, provided_outputs,
304
+ output_section = self.build_job_outputs(
305
+ required_outputs, provided_outputs,
208
306
  explicit_deps=[job_push_id])
209
307
 
210
- main_section = cls._join_sections(params_section, input_section, exec_section, output_section)
308
+ main_section = self._join_sections(params_section, input_section, exec_section, output_section)
211
309
 
212
310
  # Build job-level metadata outputs
213
311
 
@@ -215,16 +313,14 @@ class GraphBuilder:
215
313
  nid for nid, n in main_section.nodes.items()
216
314
  if isinstance(n, DataResultNode))
217
315
 
218
- result_section = cls.build_job_results(
219
- job_config, job_namespace,
220
- result_spec, bundles=data_result_ids,
316
+ result_section = self.build_job_results(
317
+ bundles=data_result_ids,
221
318
  explicit_deps=[job_push_id, *main_section.must_run])
222
319
 
223
- return cls._join_sections(main_section, result_section)
320
+ return self._join_sections(main_section, result_section)
224
321
 
225
- @classmethod
226
322
  def build_job_parameters(
227
- cls, job_namespace: NodeNamespace,
323
+ self,
228
324
  required_params: tp.Dict[str, meta.ModelParameter],
229
325
  supplied_params: tp.Dict[str, meta.Value],
230
326
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
@@ -240,18 +336,18 @@ class GraphBuilder:
240
336
  if param_schema.defaultValue is not None:
241
337
  param_def = param_schema.defaultValue
242
338
  else:
243
- raise _ex.EJobValidation(f"Missing required parameter: [{param_name}]")
339
+ self._error(_ex.EJobValidation(f"Missing required parameter: [{param_name}]"))
340
+ continue
244
341
 
245
- param_id = NodeId(param_name, job_namespace, meta.Value)
342
+ param_id = NodeId(param_name, self._job_namespace, meta.Value)
246
343
  param_node = StaticValueNode(param_id, param_def, explicit_deps=explicit_deps)
247
344
 
248
345
  nodes[param_id] = param_node
249
346
 
250
347
  return GraphSection(nodes, outputs=set(nodes.keys()), must_run=list(nodes.keys()))
251
348
 
252
- @classmethod
253
349
  def build_job_inputs(
254
- cls, job_config: config.JobConfig, job_namespace: NodeNamespace,
350
+ self,
255
351
  required_inputs: tp.Dict[str, meta.ModelInputSchema],
256
352
  supplied_inputs: tp.Dict[str, meta.TagSelector],
257
353
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
@@ -267,20 +363,21 @@ class GraphBuilder:
267
363
 
268
364
  if data_selector is None:
269
365
  if input_schema.optional:
270
- data_view_id = NodeId.of(input_name, job_namespace, _data.DataView)
366
+ data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
271
367
  nodes[data_view_id] = StaticValueNode(data_view_id, _data.DataView.create_empty())
272
368
  outputs.add(data_view_id)
273
369
  continue
274
370
  else:
275
- raise _ex.EJobValidation(f"Missing required input: [{input_name}]")
371
+ self._error(_ex.EJobValidation(f"Missing required input: [{input_name}]"))
372
+ continue
276
373
 
277
374
  # Build a data spec using metadata from the job config
278
375
  # For now we are always loading the root part, snap 0, delta 0
279
- data_def = _util.get_job_resource(data_selector, job_config).data
280
- storage_def = _util.get_job_resource(data_def.storageId, job_config).storage
376
+ data_def = _util.get_job_resource(data_selector, self._job_config).data
377
+ storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
281
378
 
282
379
  if data_def.schemaId:
283
- schema_def = _util.get_job_resource(data_def.schemaId, job_config).schema
380
+ schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
284
381
  else:
285
382
  schema_def = data_def.schema
286
383
 
@@ -289,16 +386,16 @@ class GraphBuilder:
289
386
  data_spec = _data.DataSpec(data_item, data_def, storage_def, schema_def)
290
387
 
291
388
  # Data spec node is static, using the assembled data spec
292
- data_spec_id = NodeId.of(f"{input_name}:SPEC", job_namespace, _data.DataSpec)
389
+ data_spec_id = NodeId.of(f"{input_name}:SPEC", self._job_namespace, _data.DataSpec)
293
390
  data_spec_node = StaticValueNode(data_spec_id, data_spec, explicit_deps=explicit_deps)
294
391
 
295
392
  # Physical load of data items from disk
296
393
  # Currently one item per input, since inputs are single part/delta
297
- data_load_id = NodeId.of(f"{input_name}:LOAD", job_namespace, _data.DataItem)
394
+ data_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
298
395
  data_load_node = LoadDataNode(data_load_id, data_spec_id, explicit_deps=explicit_deps)
299
396
 
300
397
  # Input views assembled by mapping one root part to each view
301
- data_view_id = NodeId.of(input_name, job_namespace, _data.DataView)
398
+ data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
302
399
  data_view_node = DataViewNode(data_view_id, schema_def, data_load_id)
303
400
 
304
401
  nodes[data_spec_id] = data_spec_node
@@ -311,9 +408,8 @@ class GraphBuilder:
311
408
 
312
409
  return GraphSection(nodes, outputs=outputs, must_run=must_run)
313
410
 
314
- @classmethod
315
411
  def build_job_outputs(
316
- cls, job_config: config.JobConfig, job_namespace: NodeNamespace,
412
+ self,
317
413
  required_outputs: tp.Dict[str, meta.ModelOutputSchema],
318
414
  supplied_outputs: tp.Dict[str, meta.TagSelector],
319
415
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
@@ -329,25 +425,27 @@ class GraphBuilder:
329
425
  if data_selector is None:
330
426
  if output_schema.optional:
331
427
  optional_info = "(configuration is required for all optional outputs, in case they are produced)"
332
- raise _ex.EJobValidation(f"Missing optional output: [{output_name}] {optional_info}")
428
+ self._error(_ex.EJobValidation(f"Missing optional output: [{output_name}] {optional_info}"))
429
+ continue
333
430
  else:
334
- raise _ex.EJobValidation(f"Missing required output: [{output_name}]")
431
+ self._error(_ex.EJobValidation(f"Missing required output: [{output_name}]"))
432
+ continue
335
433
 
336
434
  # Output data view must already exist in the namespace
337
- data_view_id = NodeId.of(output_name, job_namespace, _data.DataView)
338
- data_spec_id = NodeId.of(f"{output_name}:SPEC", job_namespace, _data.DataSpec)
435
+ data_view_id = NodeId.of(output_name, self._job_namespace, _data.DataView)
436
+ data_spec_id = NodeId.of(f"{output_name}:SPEC", self._job_namespace, _data.DataSpec)
339
437
 
340
- data_obj = _util.get_job_resource(data_selector, job_config, optional=True)
438
+ data_obj = _util.get_job_resource(data_selector, self._job_config, optional=True)
341
439
 
342
440
  if data_obj is not None:
343
441
 
344
442
  # If data def for the output has been built in advance, use a static data spec
345
443
 
346
444
  data_def = data_obj.data
347
- storage_def = _util.get_job_resource(data_def.storageId, job_config).storage
445
+ storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
348
446
 
349
447
  if data_def.schemaId:
350
- schema_def = _util.get_job_resource(data_def.schemaId, job_config).schema
448
+ schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
351
449
  else:
352
450
  schema_def = data_def.schema
353
451
 
@@ -366,28 +464,28 @@ class GraphBuilder:
366
464
  # Dynamic data def will always use an embedded schema (this is no ID for an external schema)
367
465
 
368
466
  data_key = output_name + ":DATA"
369
- data_id = job_config.resultMapping[data_key]
467
+ data_id = self._job_config.resultMapping[data_key]
370
468
  storage_key = output_name + ":STORAGE"
371
- storage_id = job_config.resultMapping[storage_key]
469
+ storage_id = self._job_config.resultMapping[storage_key]
372
470
 
373
471
  data_spec_node = DynamicDataSpecNode(
374
- data_spec_id, data_view_id,
375
- data_id, storage_id,
376
- prior_data_spec=None,
377
- explicit_deps=explicit_deps)
472
+ data_spec_id, data_view_id,
473
+ data_id, storage_id,
474
+ prior_data_spec=None,
475
+ explicit_deps=explicit_deps)
378
476
 
379
477
  output_data_key = _util.object_key(data_id)
380
478
  output_storage_key = _util.object_key(storage_id)
381
479
 
382
480
  # Map one data item from each view, since outputs are single part/delta
383
- data_item_id = NodeId(f"{output_name}:ITEM", job_namespace, _data.DataItem)
481
+ data_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
384
482
  data_item_node = DataItemNode(data_item_id, data_view_id)
385
483
 
386
484
  # Create a physical save operation for the data item
387
- data_save_id = NodeId.of(f"{output_name}:SAVE", job_namespace, None)
485
+ data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, None)
388
486
  data_save_node = SaveDataNode(data_save_id, data_spec_id, data_item_id)
389
487
 
390
- data_result_id = NodeId.of(f"{output_name}:RESULT", job_namespace, ObjectBundle)
488
+ data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
391
489
  data_result_node = DataResultNode(
392
490
  data_result_id, output_name,
393
491
  data_item_id, data_spec_id, data_save_id,
@@ -406,6 +504,9 @@ class GraphBuilder:
406
504
  @classmethod
407
505
  def build_runtime_outputs(cls, output_names: tp.List[str], job_namespace: NodeNamespace):
408
506
 
507
+ # This method is called dynamically during job execution
508
+ # So it cannot use stateful information like self._job_config or self._job_namespace
509
+
409
510
  # TODO: Factor out common logic with regular job outputs (including static / dynamic)
410
511
 
411
512
  nodes = {}
@@ -462,22 +563,21 @@ class GraphBuilder:
462
563
 
463
564
  return GraphSection(nodes, inputs=inputs, outputs={runtime_outputs_id})
464
565
 
465
- @classmethod
466
566
  def build_job_results(
467
- cls, job_config: cfg.JobConfig, job_namespace: NodeNamespace, result_spec: JobResultSpec,
567
+ self,
468
568
  objects: tp.Dict[str, NodeId[meta.ObjectDefinition]] = None,
469
569
  bundles: tp.List[NodeId[ObjectBundle]] = None,
470
570
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
471
571
  -> GraphSection:
472
572
 
473
- build_result_id = NodeId.of("trac_build_result", job_namespace, cfg.JobResult)
573
+ build_result_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
474
574
 
475
575
  if objects is not None:
476
576
 
477
577
  results_inputs = set(objects.values())
478
578
 
479
579
  build_result_node = BuildJobResultNode(
480
- build_result_id, job_config.jobId,
580
+ build_result_id, self._job_config.jobId,
481
581
  outputs = JobOutputs(objects=objects),
482
582
  explicit_deps=explicit_deps)
483
583
 
@@ -486,17 +586,16 @@ class GraphBuilder:
486
586
  results_inputs = set(bundles)
487
587
 
488
588
  build_result_node = BuildJobResultNode(
489
- build_result_id, job_config.jobId,
589
+ build_result_id, self._job_config.jobId,
490
590
  outputs = JobOutputs(bundles=bundles),
491
591
  explicit_deps=explicit_deps)
492
592
 
493
593
  else:
494
594
  raise _ex.EUnexpected()
495
595
 
496
- save_result_id = NodeId("trac_save_result", job_namespace)
497
- save_result_node = SaveJobResultNode(save_result_id, build_result_id, result_spec)
498
-
499
- if result_spec.save_result:
596
+ if self._result_spec.save_result:
597
+ save_result_id = NodeId("trac_save_result", self._job_namespace)
598
+ save_result_node = SaveJobResultNode(save_result_id, build_result_id, self._result_spec)
500
599
  result_nodes = {build_result_id: build_result_node, save_result_id: save_result_node}
501
600
  job_result_id = save_result_id
502
601
  else:
@@ -505,10 +604,9 @@ class GraphBuilder:
505
604
 
506
605
  return GraphSection(result_nodes, inputs=results_inputs, must_run=[job_result_id])
507
606
 
508
- @classmethod
509
607
  def build_model_or_flow_with_context(
510
- cls, job_config: config.JobConfig, namespace: NodeNamespace,
511
- model_or_flow_name: str, model_or_flow: meta.ObjectDefinition,
608
+ self, namespace: NodeNamespace, model_or_flow_name: str,
609
+ job_def: meta.JobDefinition, model_or_flow: meta.ObjectDefinition,
512
610
  input_mapping: tp.Dict[str, NodeId], output_mapping: tp.Dict[str, NodeId],
513
611
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
514
612
  -> GraphSection:
@@ -521,44 +619,45 @@ class GraphBuilder:
521
619
  # Execute in the sub-context by doing PUSH, EXEC, POP
522
620
  # Note that POP node must be in the sub namespace too
523
621
 
524
- push_section = cls.build_context_push(
622
+ push_section = self.build_context_push(
525
623
  sub_namespace, input_mapping,
526
624
  explicit_deps)
527
625
 
528
- exec_section = cls.build_model_or_flow(
529
- job_config, sub_namespace, model_or_flow,
626
+ exec_section = self.build_model_or_flow(
627
+ sub_namespace, job_def, model_or_flow,
530
628
  explicit_deps=push_section.must_run)
531
629
 
532
- pop_section = cls.build_context_pop(
630
+ pop_section = self.build_context_pop(
533
631
  sub_namespace, output_mapping,
534
632
  explicit_deps=exec_section.must_run)
535
633
 
536
- return cls._join_sections(push_section, exec_section, pop_section)
634
+ return self._join_sections(push_section, exec_section, pop_section)
537
635
 
538
- @classmethod
539
636
  def build_model_or_flow(
540
- cls, job_config: config.JobConfig, namespace: NodeNamespace,
637
+ self, namespace: NodeNamespace,
638
+ job_def: meta.JobDefinition,
541
639
  model_or_flow: meta.ObjectDefinition,
542
640
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
543
641
  -> GraphSection:
544
642
 
545
643
  if model_or_flow.objectType == meta.ObjectType.MODEL:
546
- return cls.build_model(job_config, namespace, model_or_flow.model, explicit_deps)
644
+ return self.build_model(namespace, job_def, model_or_flow.model, explicit_deps)
547
645
 
548
646
  elif model_or_flow.objectType == meta.ObjectType.FLOW:
549
- return cls.build_flow(job_config, namespace, model_or_flow.flow)
647
+ return self.build_flow(namespace, job_def, model_or_flow.flow)
550
648
 
551
649
  else:
552
- raise _ex.EConfigParse("Invalid job config given to the execution engine")
650
+ message = f"Invalid job config, expected model or flow, got [{model_or_flow.objectType}]"
651
+ self._error(_ex.EJobValidation(message))
553
652
 
554
- @classmethod
555
653
  def build_model(
556
- cls, job_config: config.JobConfig, namespace: NodeNamespace,
654
+ self, namespace: NodeNamespace,
655
+ job_def: meta.JobDefinition,
557
656
  model_def: meta.ModelDefinition,
558
657
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
559
658
  -> GraphSection:
560
659
 
561
- cls.check_model_type(job_config, model_def)
660
+ self.check_model_type(job_def, model_def)
562
661
 
563
662
  def param_id(node_name):
564
663
  return NodeId(node_name, namespace, meta.Value)
@@ -572,10 +671,10 @@ class GraphBuilder:
572
671
  output_ids = set(map(data_id, model_def.outputs))
573
672
 
574
673
  # Set up storage access for import / export data jobs
575
- if job_config.job.jobType == meta.JobType.IMPORT_DATA:
576
- storage_access = job_config.job.importData.storageAccess
577
- elif job_config.job.jobType == meta.JobType.EXPORT_DATA:
578
- storage_access = job_config.job.exportData.storageAccess
674
+ if job_def.jobType == meta.JobType.IMPORT_DATA:
675
+ storage_access = job_def.importData.storageAccess
676
+ elif job_def.jobType == meta.JobType.EXPORT_DATA:
677
+ storage_access = job_def.exportData.storageAccess
579
678
  else:
580
679
  storage_access = None
581
680
 
@@ -615,9 +714,9 @@ class GraphBuilder:
615
714
  # Assemble a graph to include the model and its outputs
616
715
  return GraphSection(nodes, inputs={*parameter_ids, *input_ids}, outputs=output_ids, must_run=[model_result_id])
617
716
 
618
- @classmethod
619
717
  def build_flow(
620
- cls, job_config: config.JobConfig, namespace: NodeNamespace,
718
+ self, namespace: NodeNamespace,
719
+ job_def: meta.JobDefinition,
621
720
  flow_def: meta.FlowDefinition,
622
721
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
623
722
  -> GraphSection:
@@ -650,11 +749,11 @@ class GraphBuilder:
650
749
 
651
750
  node_name, node = reachable_nodes.popitem()
652
751
 
653
- sub_section = cls.build_flow_node(
654
- job_config, namespace, target_edges,
752
+ sub_section = self.build_flow_node(
753
+ namespace, job_def, target_edges,
655
754
  node_name, node, explicit_deps)
656
755
 
657
- graph_section = cls._join_sections(graph_section, sub_section, allow_partial_inputs=True)
756
+ graph_section = self._join_sections(graph_section, sub_section, allow_partial_inputs=True)
658
757
 
659
758
  if node.nodeType != meta.FlowNodeType.OUTPUT_NODE:
660
759
 
@@ -674,20 +773,18 @@ class GraphBuilder:
674
773
  missing_targets = [edge.target for node in remaining_edges_by_target.values() for edge in node]
675
774
  missing_target_names = [f"{t.node}.{t.socket}" if t.socket else t.node for t in missing_targets]
676
775
  missing_nodes = list(map(lambda n: NodeId(n, namespace), missing_target_names))
677
- cls._invalid_graph_error(missing_nodes)
776
+ self._invalid_graph_error(missing_nodes)
678
777
 
679
778
  return graph_section
680
779
 
681
- @classmethod
682
780
  def build_flow_node(
683
- cls, job_config: config.JobConfig, namespace: NodeNamespace,
781
+ self, namespace: NodeNamespace,
782
+ job_def: meta.JobDefinition,
684
783
  target_edges: tp.Dict[meta.FlowSocket, meta.FlowEdge],
685
784
  node_name: str, node: meta.FlowNode,
686
785
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
687
786
  -> GraphSection:
688
787
 
689
- flow_job = job_config.job.runFlow
690
-
691
788
  def socket_key(socket):
692
789
  return f"{socket.node}.{socket.socket}" if socket.socket else socket.node
693
790
 
@@ -700,7 +797,7 @@ class GraphBuilder:
700
797
  edge = target_edges.get(socket)
701
798
  # Report missing edges as a job consistency error (this might happen sometimes in dev mode)
702
799
  if edge is None:
703
- raise _ex.EJobValidation(f"Inconsistent flow: Socket [{socket}] is not connected")
800
+ self._error(_ex.EJobValidation(f"Inconsistent flow: Socket [{socket}] is not connected"))
704
801
  return socket_id(edge.source.node, edge.source.socket, result_type)
705
802
 
706
803
  if node.nodeType == meta.FlowNodeType.PARAMETER_NODE:
@@ -723,27 +820,27 @@ class GraphBuilder:
723
820
  push_mapping = {**input_mapping, **param_mapping}
724
821
  pop_mapping = output_mapping
725
822
 
726
- model_selector = flow_job.models.get(node_name)
727
- model_obj = _util.get_job_resource(model_selector, job_config)
823
+ model_selector = job_def.runFlow.models.get(node_name)
824
+ model_obj = _util.get_job_resource(model_selector, self._job_config)
728
825
 
729
826
  # Missing models in the job config is a job consistency error
730
827
  if model_obj is None or model_obj.objectType != meta.ObjectType.MODEL:
731
- raise _ex.EJobValidation(f"No model was provided for flow node [{node_name}]")
828
+ self._error(_ex.EJobValidation(f"No model was provided for flow node [{node_name}]"))
732
829
 
733
830
  # Explicit check for model compatibility - report an error now, do not try build_model()
734
- cls.check_model_compatibility(model_selector, model_obj.model, node_name, node)
735
- cls.check_model_type(job_config, model_obj.model)
831
+ self.check_model_compatibility(model_selector, model_obj.model, node_name, node)
832
+ self.check_model_type(job_def, model_obj.model)
736
833
 
737
- return cls.build_model_or_flow_with_context(
738
- job_config, namespace, node_name, model_obj,
739
- push_mapping, pop_mapping, explicit_deps)
834
+ return self.build_model_or_flow_with_context(
835
+ namespace, node_name,
836
+ job_def, model_obj,
837
+ push_mapping, pop_mapping,
838
+ explicit_deps)
740
839
 
741
- # Missing / invalid node type - should be caught in static validation
742
- raise _ex.ETracInternal(f"Flow node [{node_name}] has invalid node type [{node.nodeType}]")
840
+ self._error(_ex.EJobValidation(f"Flow node [{node_name}] has invalid node type [{node.nodeType}]"))
743
841
 
744
- @classmethod
745
842
  def check_model_compatibility(
746
- cls, model_selector: meta.TagSelector,
843
+ self, model_selector: meta.TagSelector,
747
844
  model_def: meta.ModelDefinition, node_name: str, flow_node: meta.FlowNode):
748
845
 
749
846
  model_params = list(sorted(model_def.parameters.keys()))
@@ -756,22 +853,21 @@ class GraphBuilder:
756
853
 
757
854
  if model_params != node_params or model_inputs != node_inputs or model_outputs != node_outputs:
758
855
  model_key = _util.object_key(model_selector)
759
- raise _ex.EJobValidation(f"Incompatible model for flow node [{node_name}] (Model: [{model_key}])")
856
+ self._error(_ex.EJobValidation(f"Incompatible model for flow node [{node_name}] (Model: [{model_key}])"))
760
857
 
761
- @classmethod
762
- def check_model_type(cls, job_config: config.JobConfig, model_def: meta.ModelDefinition):
858
+ def check_model_type(self, job_def: meta.JobDefinition, model_def: meta.ModelDefinition):
763
859
 
764
- if job_config.job.jobType == meta.JobType.IMPORT_DATA:
860
+ if job_def.jobType == meta.JobType.IMPORT_DATA:
765
861
  allowed_model_types = [meta.ModelType.DATA_IMPORT_MODEL]
766
- elif job_config.job.jobType == meta.JobType.EXPORT_DATA:
862
+ elif job_def.jobType == meta.JobType.EXPORT_DATA:
767
863
  allowed_model_types = [meta.ModelType.DATA_EXPORT_MODEL]
768
864
  else:
769
865
  allowed_model_types = [meta.ModelType.STANDARD_MODEL]
770
866
 
771
867
  if model_def.modelType not in allowed_model_types:
772
- job_type = job_config.job.jobType.name
868
+ job_type = job_def.jobType.name
773
869
  model_type = model_def.modelType.name
774
- raise _ex.EJobValidation(f"Job type [{job_type}] cannot use model type [{model_type}]")
870
+ self._error(_ex.EJobValidation(f"Job type [{job_type}] cannot use model type [{model_type}]"))
775
871
 
776
872
  @staticmethod
777
873
  def build_context_push(
@@ -833,8 +929,7 @@ class GraphBuilder:
833
929
  outputs={*pop_mapping.values()},
834
930
  must_run=[pop_id])
835
931
 
836
- @classmethod
837
- def _join_sections(cls, *sections: GraphSection, allow_partial_inputs: bool = False):
932
+ def _join_sections(self, *sections: GraphSection, allow_partial_inputs: bool = False):
838
933
 
839
934
  n_sections = len(sections)
840
935
  first_section = sections[0]
@@ -856,7 +951,7 @@ class GraphBuilder:
856
951
  if allow_partial_inputs:
857
952
  inputs.update(requirements_not_met)
858
953
  else:
859
- cls._invalid_graph_error(requirements_not_met)
954
+ self._invalid_graph_error(requirements_not_met)
860
955
 
861
956
  nodes.update(current_section.nodes)
862
957
 
@@ -865,13 +960,12 @@ class GraphBuilder:
865
960
 
866
961
  return GraphSection(nodes, inputs, last_section.outputs, must_run)
867
962
 
868
- @classmethod
869
- def _invalid_graph_error(cls, missing_dependencies: tp.Iterable[NodeId]):
963
+ def _invalid_graph_error(self, missing_dependencies: tp.Iterable[NodeId]):
870
964
 
871
- missing_ids = ", ".join(map(cls._missing_item_display_name, missing_dependencies))
872
- message = f"Invalid job config: The execution graph has unsatisfied dependencies: [{missing_ids}]"
965
+ missing_ids = ", ".join(map(self._missing_item_display_name, missing_dependencies))
966
+ message = f"The execution graph has unsatisfied dependencies: [{missing_ids}]"
873
967
 
874
- raise _ex.EJobValidation(message)
968
+ self._error(_ex.EJobValidation(message))
875
969
 
876
970
  @classmethod
877
971
  def _missing_item_display_name(cls, node_id: NodeId):
@@ -886,3 +980,7 @@ class GraphBuilder:
886
980
  return node_id.name
887
981
  else:
888
982
  return f"{node_id.name} / {', '.join(components[:-1])}"
983
+
984
+ def _error(self, error: Exception):
985
+
986
+ self._errors.append(error)