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.
- tracdap/rt/__init__.py +6 -5
- tracdap/rt/_exec/actors.py +6 -5
- tracdap/rt/_exec/context.py +278 -110
- tracdap/rt/_exec/dev_mode.py +237 -143
- tracdap/rt/_exec/engine.py +223 -64
- tracdap/rt/_exec/functions.py +31 -6
- tracdap/rt/_exec/graph.py +15 -5
- tracdap/rt/_exec/graph_builder.py +301 -203
- tracdap/rt/_exec/runtime.py +13 -10
- tracdap/rt/_exec/server.py +6 -5
- tracdap/rt/_impl/__init__.py +6 -5
- tracdap/rt/_impl/config_parser.py +17 -9
- tracdap/rt/_impl/data.py +284 -172
- tracdap/rt/_impl/ext/__init__.py +14 -0
- tracdap/rt/_impl/ext/sql.py +117 -0
- tracdap/rt/_impl/ext/storage.py +58 -0
- tracdap/rt/_impl/grpc/__init__.py +6 -5
- tracdap/rt/_impl/grpc/codec.py +6 -5
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +62 -54
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +37 -2
- tracdap/rt/_impl/guard_rails.py +6 -5
- tracdap/rt/_impl/models.py +6 -5
- tracdap/rt/_impl/repos.py +6 -5
- tracdap/rt/_impl/schemas.py +6 -5
- tracdap/rt/_impl/shim.py +6 -5
- tracdap/rt/_impl/static_api.py +30 -16
- tracdap/rt/_impl/storage.py +8 -7
- tracdap/rt/_impl/type_system.py +6 -5
- tracdap/rt/_impl/util.py +16 -5
- tracdap/rt/_impl/validation.py +72 -18
- tracdap/rt/_plugins/__init__.py +6 -5
- tracdap/rt/_plugins/_helpers.py +6 -5
- tracdap/rt/_plugins/config_local.py +6 -5
- tracdap/rt/_plugins/format_arrow.py +6 -5
- tracdap/rt/_plugins/format_csv.py +6 -5
- tracdap/rt/_plugins/format_parquet.py +6 -5
- tracdap/rt/_plugins/repo_git.py +6 -5
- tracdap/rt/_plugins/repo_local.py +6 -5
- tracdap/rt/_plugins/repo_pypi.py +6 -5
- tracdap/rt/_plugins/storage_aws.py +6 -5
- tracdap/rt/_plugins/storage_azure.py +6 -5
- tracdap/rt/_plugins/storage_gcp.py +6 -5
- tracdap/rt/_plugins/storage_local.py +6 -5
- tracdap/rt/_plugins/storage_sql.py +418 -0
- tracdap/rt/_plugins/storage_sql_dialects.py +118 -0
- tracdap/rt/_version.py +7 -6
- tracdap/rt/api/__init__.py +23 -5
- tracdap/rt/api/experimental.py +85 -37
- tracdap/rt/api/hook.py +16 -5
- tracdap/rt/api/model_api.py +110 -90
- tracdap/rt/api/static_api.py +142 -100
- tracdap/rt/config/common.py +26 -27
- tracdap/rt/config/job.py +5 -6
- tracdap/rt/config/platform.py +41 -42
- tracdap/rt/config/result.py +5 -6
- tracdap/rt/config/runtime.py +6 -7
- tracdap/rt/exceptions.py +13 -7
- tracdap/rt/ext/__init__.py +6 -5
- tracdap/rt/ext/config.py +6 -5
- tracdap/rt/ext/embed.py +6 -5
- tracdap/rt/ext/plugins.py +6 -5
- tracdap/rt/ext/repos.py +6 -5
- tracdap/rt/ext/storage.py +6 -5
- tracdap/rt/launch/__init__.py +10 -5
- tracdap/rt/launch/__main__.py +6 -5
- tracdap/rt/launch/cli.py +6 -5
- tracdap/rt/launch/launch.py +38 -15
- tracdap/rt/metadata/__init__.py +4 -0
- tracdap/rt/metadata/common.py +2 -3
- tracdap/rt/metadata/custom.py +3 -4
- tracdap/rt/metadata/data.py +30 -31
- tracdap/rt/metadata/file.py +6 -7
- tracdap/rt/metadata/flow.py +22 -23
- tracdap/rt/metadata/job.py +89 -45
- tracdap/rt/metadata/model.py +26 -27
- tracdap/rt/metadata/object.py +11 -12
- tracdap/rt/metadata/object_id.py +23 -24
- tracdap/rt/metadata/resource.py +0 -1
- tracdap/rt/metadata/search.py +15 -16
- tracdap/rt/metadata/stoarge.py +22 -23
- tracdap/rt/metadata/tag.py +8 -9
- tracdap/rt/metadata/tag_update.py +11 -12
- tracdap/rt/metadata/type.py +38 -38
- {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/LICENSE +1 -1
- {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/METADATA +4 -2
- tracdap_runtime-0.7.0.dist-info/RECORD +121 -0
- {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/WHEEL +1 -1
- tracdap_runtime-0.6.5.dist-info/RECORD +0 -116
- {tracdap_runtime-0.6.5.dist-info → tracdap_runtime-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,9 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
38
|
-
|
38
|
+
self._job_config = job_config
|
39
|
+
self._result_spec = result_spec
|
39
40
|
|
40
|
-
|
41
|
-
|
41
|
+
self._job_key = _util.object_key(job_config.jobId)
|
42
|
+
self._job_namespace = NodeNamespace(self._job_key)
|
42
43
|
|
43
|
-
|
44
|
-
return cls.build_standard_job(job_config, result_spec, cls.build_import_export_data_job)
|
44
|
+
self._errors = []
|
45
45
|
|
46
|
-
|
46
|
+
def _child_builder(self, job_id: meta.TagHeader) -> "GraphBuilder":
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
59
|
-
|
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(
|
65
|
-
main_result_id = NodeId.of("
|
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(
|
110
|
+
global_result_id = NodeId.of(self._job_key, NodeNamespace.root(), config.JobResult)
|
70
111
|
|
71
|
-
pop_id = NodeId("trac_job_pop",
|
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,
|
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,
|
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 =
|
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
|
-
|
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 =
|
102
|
-
import_details =
|
138
|
+
model_scope = self._job_key
|
139
|
+
import_details = job_def.importModel
|
103
140
|
|
104
|
-
import_id = NodeId.of("trac_import_model",
|
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 =
|
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
|
152
|
+
return self._join_sections(main_section, result_section)
|
117
153
|
|
118
|
-
|
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
|
128
|
-
|
159
|
+
if job_def.jobType == meta.JobType.IMPORT_DATA:
|
160
|
+
job_details = job_def.importData
|
129
161
|
else:
|
130
|
-
|
162
|
+
job_details = job_def.exportData
|
131
163
|
|
132
|
-
target_selector =
|
133
|
-
target_obj = _util.get_job_resource(target_selector,
|
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
|
137
|
-
|
138
|
-
target_selector, target_def,
|
168
|
+
return self.build_calculation_job(
|
169
|
+
job_def, job_push_id,
|
170
|
+
target_selector, target_def,
|
171
|
+
job_details)
|
139
172
|
|
140
|
-
|
141
|
-
|
142
|
-
|
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 =
|
147
|
-
target_obj = _util.get_job_resource(target_selector,
|
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
|
152
|
-
|
153
|
-
target_selector, target_def,
|
181
|
+
return self.build_calculation_job(
|
182
|
+
job_def, job_push_id,
|
183
|
+
target_selector, target_def,
|
184
|
+
job_details)
|
154
185
|
|
155
|
-
|
156
|
-
|
157
|
-
|
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 =
|
162
|
-
target_obj = _util.get_job_resource(target_selector,
|
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
|
167
|
-
|
168
|
-
target_selector, target_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
|
-
|
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
|
-
|
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 =
|
189
|
-
provided_inputs =
|
190
|
-
provided_outputs =
|
285
|
+
provided_params = job_details.parameters
|
286
|
+
provided_inputs = job_details.inputs
|
287
|
+
provided_outputs = job_details.outputs
|
191
288
|
|
192
|
-
params_section =
|
193
|
-
|
289
|
+
params_section = self.build_job_parameters(
|
290
|
+
required_params, provided_params,
|
194
291
|
explicit_deps=[job_push_id])
|
195
292
|
|
196
|
-
input_section =
|
197
|
-
|
293
|
+
input_section = self.build_job_inputs(
|
294
|
+
required_inputs, provided_inputs,
|
198
295
|
explicit_deps=[job_push_id])
|
199
296
|
|
200
|
-
|
297
|
+
exec_namespace = self._job_namespace
|
298
|
+
exec_obj = _util.get_job_resource(target_selector, self._job_config)
|
201
299
|
|
202
|
-
exec_section =
|
203
|
-
|
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 =
|
207
|
-
|
304
|
+
output_section = self.build_job_outputs(
|
305
|
+
required_outputs, provided_outputs,
|
208
306
|
explicit_deps=[job_push_id])
|
209
307
|
|
210
|
-
main_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 =
|
219
|
-
|
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
|
320
|
+
return self._join_sections(main_section, result_section)
|
224
321
|
|
225
|
-
@classmethod
|
226
322
|
def build_job_parameters(
|
227
|
-
|
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
|
-
|
339
|
+
self._error(_ex.EJobValidation(f"Missing required parameter: [{param_name}]"))
|
340
|
+
continue
|
244
341
|
|
245
|
-
param_id = NodeId(param_name,
|
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
|
-
|
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,
|
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
|
-
|
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,
|
280
|
-
storage_def = _util.get_job_resource(data_def.storageId,
|
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,
|
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",
|
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",
|
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,
|
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
|
-
|
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
|
-
|
428
|
+
self._error(_ex.EJobValidation(f"Missing optional output: [{output_name}] {optional_info}"))
|
429
|
+
continue
|
333
430
|
else:
|
334
|
-
|
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,
|
338
|
-
data_spec_id = NodeId.of(f"{output_name}:SPEC",
|
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,
|
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,
|
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,
|
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 =
|
467
|
+
data_id = self._job_config.resultMapping[data_key]
|
370
468
|
storage_key = output_name + ":STORAGE"
|
371
|
-
storage_id =
|
469
|
+
storage_id = self._job_config.resultMapping[storage_key]
|
372
470
|
|
373
471
|
data_spec_node = DynamicDataSpecNode(
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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",
|
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",
|
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",
|
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
|
-
|
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("
|
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,
|
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,
|
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
|
-
|
497
|
-
|
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
|
-
|
511
|
-
|
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 =
|
622
|
+
push_section = self.build_context_push(
|
525
623
|
sub_namespace, input_mapping,
|
526
624
|
explicit_deps)
|
527
625
|
|
528
|
-
exec_section =
|
529
|
-
|
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 =
|
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
|
634
|
+
return self._join_sections(push_section, exec_section, pop_section)
|
537
635
|
|
538
|
-
@classmethod
|
539
636
|
def build_model_or_flow(
|
540
|
-
|
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
|
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
|
647
|
+
return self.build_flow(namespace, job_def, model_or_flow.flow)
|
550
648
|
|
551
649
|
else:
|
552
|
-
|
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
|
-
|
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
|
-
|
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
|
576
|
-
storage_access =
|
577
|
-
elif
|
578
|
-
storage_access =
|
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
|
-
|
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 =
|
654
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
727
|
-
model_obj = _util.get_job_resource(model_selector,
|
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
|
-
|
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
|
-
|
735
|
-
|
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
|
738
|
-
|
739
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
856
|
+
self._error(_ex.EJobValidation(f"Incompatible model for flow node [{node_name}] (Model: [{model_key}])"))
|
760
857
|
|
761
|
-
|
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
|
860
|
+
if job_def.jobType == meta.JobType.IMPORT_DATA:
|
765
861
|
allowed_model_types = [meta.ModelType.DATA_IMPORT_MODEL]
|
766
|
-
elif
|
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 =
|
868
|
+
job_type = job_def.jobType.name
|
773
869
|
model_type = model_def.modelType.name
|
774
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
872
|
-
message = f"
|
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
|
-
|
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)
|