tracdap-runtime 0.7.0__py3-none-any.whl → 0.8.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tracdap/rt/_exec/context.py +140 -64
- tracdap/rt/_exec/dev_mode.py +144 -69
- tracdap/rt/_exec/engine.py +9 -7
- tracdap/rt/_exec/functions.py +95 -33
- tracdap/rt/_exec/graph.py +22 -15
- tracdap/rt/_exec/graph_builder.py +221 -98
- tracdap/rt/_exec/runtime.py +19 -6
- tracdap/rt/_impl/data.py +86 -13
- tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.py +3 -1
- tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.pyi +8 -0
- tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +27 -25
- tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -4
- tracdap/rt/_impl/models.py +9 -7
- tracdap/rt/_impl/static_api.py +53 -33
- tracdap/rt/_impl/util.py +1 -1
- tracdap/rt/_impl/validation.py +54 -28
- tracdap/rt/_version.py +1 -1
- tracdap/rt/api/__init__.py +6 -3
- tracdap/rt/api/file_types.py +29 -0
- tracdap/rt/api/hook.py +15 -7
- tracdap/rt/api/model_api.py +16 -0
- tracdap/rt/api/static_api.py +211 -125
- tracdap/rt/config/__init__.py +6 -6
- tracdap/rt/config/common.py +11 -1
- tracdap/rt/config/platform.py +4 -6
- tracdap/rt/launch/launch.py +9 -11
- tracdap/rt/metadata/__init__.py +10 -9
- tracdap/rt/metadata/file.py +8 -0
- tracdap/rt/metadata/model.py +12 -2
- {tracdap_runtime-0.7.0.dist-info → tracdap_runtime-0.8.0b1.dist-info}/METADATA +15 -15
- {tracdap_runtime-0.7.0.dist-info → tracdap_runtime-0.8.0b1.dist-info}/RECORD +34 -33
- {tracdap_runtime-0.7.0.dist-info → tracdap_runtime-0.8.0b1.dist-info}/WHEEL +1 -1
- {tracdap_runtime-0.7.0.dist-info → tracdap_runtime-0.8.0b1.dist-info}/LICENSE +0 -0
- {tracdap_runtime-0.7.0.dist-info → tracdap_runtime-0.8.0b1.dist-info}/top_level.txt +0 -0
tracdap/rt/_exec/graph.py
CHANGED
@@ -309,20 +309,18 @@ class DataItemNode(MappingNode[_data.DataItem]):
|
|
309
309
|
@_node_type
|
310
310
|
class DataResultNode(Node[ObjectBundle]):
|
311
311
|
|
312
|
+
# TODO: Remove this node type
|
313
|
+
# Either produce metadata in SaveDataNode, or handle DataSpec outputs in result processing nodes
|
314
|
+
|
312
315
|
output_name: str
|
313
|
-
|
314
|
-
data_spec_id: NodeId[_data.DataSpec]
|
315
|
-
data_save_id: NodeId[type(None)]
|
316
|
+
data_save_id: NodeId[_data.DataSpec]
|
316
317
|
|
317
|
-
data_key: str
|
318
|
-
|
318
|
+
data_key: str = None
|
319
|
+
file_key: str = None
|
320
|
+
storage_key: str = None
|
319
321
|
|
320
322
|
def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
|
321
|
-
|
322
|
-
return {
|
323
|
-
self.data_item_id: DependencyType.HARD,
|
324
|
-
self.data_spec_id: DependencyType.HARD,
|
325
|
-
self.data_save_id: DependencyType.HARD}
|
323
|
+
return {self.data_save_id: DependencyType.HARD}
|
326
324
|
|
327
325
|
|
328
326
|
@_node_type
|
@@ -333,24 +331,33 @@ class LoadDataNode(Node[_data.DataItem]):
|
|
333
331
|
The latest incarnation of the item will be loaded from any available copy
|
334
332
|
"""
|
335
333
|
|
336
|
-
spec_id: NodeId[_data.DataSpec]
|
334
|
+
spec_id: tp.Optional[NodeId[_data.DataSpec]] = None
|
335
|
+
spec: tp.Optional[_data.DataSpec] = None
|
337
336
|
|
338
337
|
def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
|
339
|
-
|
338
|
+
deps = dict()
|
339
|
+
if self.spec_id is not None:
|
340
|
+
deps[self.spec_id] = DependencyType.HARD
|
341
|
+
return deps
|
340
342
|
|
341
343
|
|
342
344
|
@_node_type
|
343
|
-
class SaveDataNode(Node[
|
345
|
+
class SaveDataNode(Node[_data.DataSpec]):
|
344
346
|
|
345
347
|
"""
|
346
348
|
Save an individual data item to storage
|
347
349
|
"""
|
348
350
|
|
349
|
-
spec_id: NodeId[_data.DataSpec]
|
350
351
|
data_item_id: NodeId[_data.DataItem]
|
351
352
|
|
353
|
+
spec_id: tp.Optional[NodeId[_data.DataSpec]] = None
|
354
|
+
spec: tp.Optional[_data.DataSpec] = None
|
355
|
+
|
352
356
|
def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
|
353
|
-
|
357
|
+
deps = {self.data_item_id: DependencyType.HARD}
|
358
|
+
if self.spec_id is not None:
|
359
|
+
deps[self.spec_id] = DependencyType.HARD
|
360
|
+
return deps
|
354
361
|
|
355
362
|
|
356
363
|
@_node_type
|
@@ -13,6 +13,8 @@
|
|
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
|
17
|
+
|
16
18
|
import tracdap.rt.config as config
|
17
19
|
import tracdap.rt.exceptions as _ex
|
18
20
|
import tracdap.rt._impl.data as _data # noqa
|
@@ -33,8 +35,9 @@ class GraphBuilder:
|
|
33
35
|
|
34
36
|
__JOB_BUILD_FUNC = tp.Callable[[meta.JobDefinition, NodeId], GraphSection]
|
35
37
|
|
36
|
-
def __init__(self, job_config: config.JobConfig, result_spec: JobResultSpec):
|
38
|
+
def __init__(self, sys_config: config.RuntimeConfig, job_config: config.JobConfig, result_spec: JobResultSpec):
|
37
39
|
|
40
|
+
self._sys_config = sys_config
|
38
41
|
self._job_config = job_config
|
39
42
|
self._result_spec = result_spec
|
40
43
|
|
@@ -45,7 +48,7 @@ class GraphBuilder:
|
|
45
48
|
|
46
49
|
def _child_builder(self, job_id: meta.TagHeader) -> "GraphBuilder":
|
47
50
|
|
48
|
-
builder = GraphBuilder(self._job_config, JobResultSpec(save_result=False))
|
51
|
+
builder = GraphBuilder(self._sys_config, self._job_config, JobResultSpec(save_result=False))
|
49
52
|
builder._job_key = _util.object_key(job_id)
|
50
53
|
builder._job_namespace = NodeNamespace(builder._job_key)
|
51
54
|
|
@@ -355,58 +358,76 @@ class GraphBuilder:
|
|
355
358
|
|
356
359
|
nodes = dict()
|
357
360
|
outputs = set()
|
358
|
-
must_run = list()
|
359
361
|
|
360
|
-
for input_name,
|
362
|
+
for input_name, input_def in required_inputs.items():
|
363
|
+
|
364
|
+
# Backwards compatibility with pre 0.8 versions
|
365
|
+
input_type = meta.ObjectType.DATA \
|
366
|
+
if input_def.objectType == meta.ObjectType.OBJECT_TYPE_NOT_SET \
|
367
|
+
else input_def.objectType
|
368
|
+
|
369
|
+
input_selector = supplied_inputs.get(input_name)
|
361
370
|
|
362
|
-
|
371
|
+
if input_selector is None:
|
363
372
|
|
364
|
-
|
365
|
-
if input_schema.optional:
|
373
|
+
if input_def.optional:
|
366
374
|
data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
367
|
-
|
375
|
+
data_view = _data.DataView.create_empty(input_type)
|
376
|
+
nodes[data_view_id] = StaticValueNode(data_view_id, data_view, explicit_deps=explicit_deps)
|
368
377
|
outputs.add(data_view_id)
|
369
|
-
continue
|
370
378
|
else:
|
371
379
|
self._error(_ex.EJobValidation(f"Missing required input: [{input_name}]"))
|
372
|
-
continue
|
373
380
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
381
|
+
elif input_type == meta.ObjectType.DATA:
|
382
|
+
self._build_data_input(input_name, input_selector, nodes, outputs, explicit_deps)
|
383
|
+
|
384
|
+
elif input_type == meta.ObjectType.FILE:
|
385
|
+
self._build_file_input(input_name, input_selector, nodes, outputs, explicit_deps)
|
378
386
|
|
379
|
-
if data_def.schemaId:
|
380
|
-
schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
|
381
387
|
else:
|
382
|
-
|
388
|
+
self._error(_ex.EJobValidation(f"Invalid input type [{input_type.name}] for input [{input_name}]"))
|
383
389
|
|
384
|
-
|
385
|
-
data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
|
386
|
-
data_spec = _data.DataSpec(data_item, data_def, storage_def, schema_def)
|
390
|
+
return GraphSection(nodes, outputs=outputs)
|
387
391
|
|
388
|
-
|
389
|
-
data_spec_id = NodeId.of(f"{input_name}:SPEC", self._job_namespace, _data.DataSpec)
|
390
|
-
data_spec_node = StaticValueNode(data_spec_id, data_spec, explicit_deps=explicit_deps)
|
392
|
+
def _build_data_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
|
391
393
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
394
|
+
# Build a data spec using metadata from the job config
|
395
|
+
# For now we are always loading the root part, snap 0, delta 0
|
396
|
+
data_def = _util.get_job_resource(input_selector, self._job_config).data
|
397
|
+
storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
|
396
398
|
|
397
|
-
|
398
|
-
|
399
|
-
|
399
|
+
if data_def.schemaId:
|
400
|
+
schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
|
401
|
+
else:
|
402
|
+
schema_def = data_def.schema
|
400
403
|
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
+
root_part_opaque_key = 'part-root' # TODO: Central part names / constants
|
405
|
+
data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
|
406
|
+
data_spec = _data.DataSpec.create_data_spec(data_item, data_def, storage_def, schema_def)
|
407
|
+
|
408
|
+
# Physical load of data items from disk
|
409
|
+
# Currently one item per input, since inputs are single part/delta
|
410
|
+
data_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
|
411
|
+
nodes[data_load_id] = LoadDataNode(data_load_id, spec=data_spec, explicit_deps=explicit_deps)
|
412
|
+
|
413
|
+
# Input views assembled by mapping one root part to each view
|
414
|
+
data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
415
|
+
nodes[data_view_id] = DataViewNode(data_view_id, schema_def, data_load_id)
|
416
|
+
outputs.add(data_view_id)
|
417
|
+
|
418
|
+
def _build_file_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
|
404
419
|
|
405
|
-
|
406
|
-
|
407
|
-
must_run.append(data_spec_id)
|
420
|
+
file_def = _util.get_job_resource(input_selector, self._job_config).file
|
421
|
+
storage_def = _util.get_job_resource(file_def.storageId, self._job_config).storage
|
408
422
|
|
409
|
-
|
423
|
+
file_spec = _data.DataSpec.create_file_spec(file_def.dataItem, file_def, storage_def)
|
424
|
+
file_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
|
425
|
+
nodes[file_load_id] = LoadDataNode(file_load_id, spec=file_spec, explicit_deps=explicit_deps)
|
426
|
+
|
427
|
+
# Input views assembled by mapping one root part to each view
|
428
|
+
file_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
|
429
|
+
nodes[file_view_id] = DataViewNode(file_view_id, None, file_load_id)
|
430
|
+
outputs.add(file_view_id)
|
410
431
|
|
411
432
|
def build_job_outputs(
|
412
433
|
self,
|
@@ -418,12 +439,21 @@ class GraphBuilder:
|
|
418
439
|
nodes = {}
|
419
440
|
inputs = set()
|
420
441
|
|
421
|
-
for output_name,
|
442
|
+
for output_name, output_def in required_outputs.items():
|
443
|
+
|
444
|
+
# Output data view must already exist in the namespace, it is an input to the save operation
|
445
|
+
data_view_id = NodeId.of(output_name, self._job_namespace, _data.DataView)
|
446
|
+
inputs.add(data_view_id)
|
447
|
+
|
448
|
+
# Backwards compatibility with pre 0.8 versions
|
449
|
+
output_type = meta.ObjectType.DATA \
|
450
|
+
if output_def.objectType == meta.ObjectType.OBJECT_TYPE_NOT_SET \
|
451
|
+
else output_def.objectType
|
422
452
|
|
423
|
-
|
453
|
+
output_selector = supplied_outputs.get(output_name)
|
424
454
|
|
425
|
-
if
|
426
|
-
if
|
455
|
+
if output_selector is None:
|
456
|
+
if output_def.optional:
|
427
457
|
optional_info = "(configuration is required for all optional outputs, in case they are produced)"
|
428
458
|
self._error(_ex.EJobValidation(f"Missing optional output: [{output_name}] {optional_info}"))
|
429
459
|
continue
|
@@ -431,75 +461,129 @@ class GraphBuilder:
|
|
431
461
|
self._error(_ex.EJobValidation(f"Missing required output: [{output_name}]"))
|
432
462
|
continue
|
433
463
|
|
434
|
-
|
435
|
-
|
436
|
-
data_spec_id = NodeId.of(f"{output_name}:SPEC", self._job_namespace, _data.DataSpec)
|
464
|
+
elif output_type == meta.ObjectType.DATA:
|
465
|
+
self._build_data_output(output_name, output_selector, data_view_id, nodes, explicit_deps)
|
437
466
|
|
438
|
-
|
467
|
+
elif output_type == meta.ObjectType.FILE:
|
468
|
+
self._build_file_output(output_name, output_def, output_selector, data_view_id, nodes, explicit_deps)
|
439
469
|
|
440
|
-
|
470
|
+
else:
|
471
|
+
self._error(_ex.EJobValidation(f"Invalid output type [{output_type.name}] for input [{output_name}]"))
|
441
472
|
|
442
|
-
|
473
|
+
return GraphSection(nodes, inputs=inputs)
|
443
474
|
|
444
|
-
|
445
|
-
storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
|
475
|
+
def _build_data_output(self, output_name, output_selector, data_view_id, nodes, explicit_deps):
|
446
476
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
schema_def = data_def.schema
|
477
|
+
# Map one data item from each view, since outputs are single part/delta
|
478
|
+
data_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
|
479
|
+
nodes[data_item_id] = DataItemNode(data_item_id, data_view_id)
|
451
480
|
|
452
|
-
|
453
|
-
data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
|
454
|
-
data_spec = _data.DataSpec(data_item, data_def, storage_def, schema_def)
|
481
|
+
data_obj = _util.get_job_resource(output_selector, self._job_config, optional=True)
|
455
482
|
|
456
|
-
|
483
|
+
if data_obj is not None:
|
457
484
|
|
458
|
-
|
459
|
-
output_storage_key = output_name + ":STORAGE"
|
485
|
+
# If data def for the output has been built in advance, use a static data spec
|
460
486
|
|
487
|
+
data_def = data_obj.data
|
488
|
+
storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
|
489
|
+
|
490
|
+
if data_def.schemaId:
|
491
|
+
schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
|
461
492
|
else:
|
493
|
+
schema_def = data_def.schema
|
462
494
|
|
463
|
-
|
464
|
-
|
495
|
+
root_part_opaque_key = 'part-root' # TODO: Central part names / constants
|
496
|
+
data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
|
497
|
+
data_spec = _data.DataSpec.create_data_spec(data_item, data_def, storage_def, schema_def)
|
498
|
+
|
499
|
+
# Create a physical save operation for the data item
|
500
|
+
data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
|
501
|
+
nodes[data_save_id] = SaveDataNode(data_save_id, data_item_id, spec=data_spec)
|
502
|
+
|
503
|
+
output_key = output_name
|
504
|
+
storage_key = output_name + ":STORAGE"
|
465
505
|
|
466
|
-
|
467
|
-
data_id = self._job_config.resultMapping[data_key]
|
468
|
-
storage_key = output_name + ":STORAGE"
|
469
|
-
storage_id = self._job_config.resultMapping[storage_key]
|
506
|
+
else:
|
470
507
|
|
471
|
-
|
472
|
-
|
473
|
-
data_id, storage_id,
|
474
|
-
prior_data_spec=None,
|
475
|
-
explicit_deps=explicit_deps)
|
508
|
+
# If output data def for an output was not supplied in the job, create a dynamic data spec
|
509
|
+
# Dynamic data def will always use an embedded schema (this is no ID for an external schema)
|
476
510
|
|
477
|
-
|
478
|
-
|
511
|
+
mapped_output_key = output_name
|
512
|
+
mapped_storage_key = output_name + ":STORAGE"
|
479
513
|
|
480
|
-
|
481
|
-
|
482
|
-
|
514
|
+
data_id = self._job_config.resultMapping[mapped_output_key]
|
515
|
+
storage_id = self._job_config.resultMapping[mapped_storage_key]
|
516
|
+
|
517
|
+
data_spec_id = NodeId.of(f"{output_name}:SPEC", self._job_namespace, _data.DataSpec)
|
518
|
+
nodes[data_spec_id] = DynamicDataSpecNode(
|
519
|
+
data_spec_id, data_view_id,
|
520
|
+
data_id, storage_id,
|
521
|
+
prior_data_spec=None,
|
522
|
+
explicit_deps=explicit_deps)
|
483
523
|
|
484
524
|
# Create a physical save operation for the data item
|
485
|
-
data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace,
|
486
|
-
|
525
|
+
data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
|
526
|
+
nodes[data_save_id] = SaveDataNode(data_save_id, data_item_id, spec_id=data_spec_id)
|
487
527
|
|
488
|
-
|
489
|
-
|
490
|
-
data_result_id, output_name,
|
491
|
-
data_item_id, data_spec_id, data_save_id,
|
492
|
-
output_data_key, output_storage_key)
|
528
|
+
output_key = _util.object_key(data_id)
|
529
|
+
storage_key = _util.object_key(storage_id)
|
493
530
|
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
531
|
+
data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
|
532
|
+
nodes[data_result_id] = DataResultNode(
|
533
|
+
data_result_id, output_name, data_save_id,
|
534
|
+
data_key=output_key,
|
535
|
+
storage_key=storage_key)
|
498
536
|
|
499
|
-
|
500
|
-
inputs.add(data_view_id)
|
537
|
+
def _build_file_output(self, output_name, output_def, output_selector, file_view_id, nodes, explicit_deps):
|
501
538
|
|
502
|
-
|
539
|
+
mapped_output_key = output_name
|
540
|
+
mapped_storage_key = output_name + ":STORAGE"
|
541
|
+
|
542
|
+
file_obj = _util.get_job_resource(output_selector, self._job_config, optional=True)
|
543
|
+
|
544
|
+
if file_obj is not None:
|
545
|
+
|
546
|
+
# Definitions already exist (generated by dev mode translator)
|
547
|
+
|
548
|
+
file_def = _util.get_job_resource(output_selector, self._job_config).file
|
549
|
+
storage_def = _util.get_job_resource(file_def.storageId, self._job_config).storage
|
550
|
+
|
551
|
+
resolved_output_key = mapped_output_key
|
552
|
+
resolved_storage_key = mapped_storage_key
|
553
|
+
|
554
|
+
else:
|
555
|
+
|
556
|
+
# Create new definitions (default behavior for jobs sent from the platform)
|
557
|
+
|
558
|
+
output_id = self._job_config.resultMapping[mapped_output_key]
|
559
|
+
storage_id = self._job_config.resultMapping[mapped_storage_key]
|
560
|
+
|
561
|
+
file_type = output_def.fileType
|
562
|
+
timestamp = _dt.datetime.fromisoformat(output_id.objectTimestamp.isoDatetime)
|
563
|
+
data_item = f"file/{output_id.objectId}/version-{output_id.objectVersion}"
|
564
|
+
storage_key = self._sys_config.storage.defaultBucket
|
565
|
+
storage_path = f"file/FILE-{output_id.objectId}/version-{output_id.objectVersion}/{output_name}.{file_type.extension}"
|
566
|
+
|
567
|
+
file_def = self.build_file_def(output_name, file_type, storage_id, data_item)
|
568
|
+
storage_def = self.build_storage_def(data_item, storage_key, storage_path, file_type.mimeType, timestamp)
|
569
|
+
|
570
|
+
resolved_output_key = _util.object_key(output_id)
|
571
|
+
resolved_storage_key = _util.object_key(storage_id)
|
572
|
+
|
573
|
+
# Required object defs are available, now build the graph nodes
|
574
|
+
|
575
|
+
file_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
|
576
|
+
nodes[file_item_id] = DataItemNode(file_item_id, file_view_id, explicit_deps=explicit_deps)
|
577
|
+
|
578
|
+
file_spec = _data.DataSpec.create_file_spec(file_def.dataItem, file_def, storage_def)
|
579
|
+
file_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
|
580
|
+
nodes[file_save_id] = SaveDataNode(file_save_id, file_item_id, spec=file_spec)
|
581
|
+
|
582
|
+
data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
|
583
|
+
nodes[data_result_id] = DataResultNode(
|
584
|
+
data_result_id, output_name, file_save_id,
|
585
|
+
file_key=resolved_output_key,
|
586
|
+
storage_key=resolved_storage_key)
|
503
587
|
|
504
588
|
@classmethod
|
505
589
|
def build_runtime_outputs(cls, output_names: tp.List[str], job_namespace: NodeNamespace):
|
@@ -519,9 +603,10 @@ class GraphBuilder:
|
|
519
603
|
data_view_id = NodeId.of(output_name, job_namespace, _data.DataView)
|
520
604
|
data_spec_id = NodeId.of(f"{output_name}:SPEC", job_namespace, _data.DataSpec)
|
521
605
|
|
522
|
-
|
606
|
+
mapped_output_key = output_name
|
607
|
+
mapped_storage_key = output_name + ":STORAGE"
|
608
|
+
|
523
609
|
data_id = _util.new_object_id(meta.ObjectType.DATA)
|
524
|
-
storage_key = output_name + ":STORAGE"
|
525
610
|
storage_id = _util.new_object_id(meta.ObjectType.STORAGE)
|
526
611
|
|
527
612
|
data_spec_node = DynamicDataSpecNode(
|
@@ -529,22 +614,21 @@ class GraphBuilder:
|
|
529
614
|
data_id, storage_id,
|
530
615
|
prior_data_spec=None)
|
531
616
|
|
532
|
-
|
533
|
-
|
617
|
+
output_key = _util.object_key(data_id)
|
618
|
+
storage_key = _util.object_key(storage_id)
|
534
619
|
|
535
620
|
# Map one data item from each view, since outputs are single part/delta
|
536
621
|
data_item_id = NodeId(f"{output_name}:ITEM", job_namespace, _data.DataItem)
|
537
622
|
data_item_node = DataItemNode(data_item_id, data_view_id)
|
538
623
|
|
539
624
|
# Create a physical save operation for the data item
|
540
|
-
data_save_id = NodeId.of(f"{output_name}:SAVE", job_namespace,
|
541
|
-
data_save_node = SaveDataNode(data_save_id,
|
625
|
+
data_save_id = NodeId.of(f"{output_name}:SAVE", job_namespace, _data.DataSpec)
|
626
|
+
data_save_node = SaveDataNode(data_save_id, data_item_id, spec_id=data_spec_id)
|
542
627
|
|
543
628
|
data_result_id = NodeId.of(f"{output_name}:RESULT", job_namespace, ObjectBundle)
|
544
629
|
data_result_node = DataResultNode(
|
545
|
-
data_result_id, output_name,
|
546
|
-
|
547
|
-
output_data_key, output_storage_key)
|
630
|
+
data_result_id, output_name, data_save_id,
|
631
|
+
output_key, storage_key)
|
548
632
|
|
549
633
|
nodes[data_spec_id] = data_spec_node
|
550
634
|
nodes[data_item_id] = data_item_node
|
@@ -563,6 +647,45 @@ class GraphBuilder:
|
|
563
647
|
|
564
648
|
return GraphSection(nodes, inputs=inputs, outputs={runtime_outputs_id})
|
565
649
|
|
650
|
+
@classmethod
|
651
|
+
def build_file_def(cls, file_name, file_type, storage_id, data_item):
|
652
|
+
|
653
|
+
file_def = meta.FileDefinition()
|
654
|
+
file_def.name = f"{file_name}.{file_type.extension}"
|
655
|
+
file_def.extension = file_type.extension
|
656
|
+
file_def.mimeType = file_type.mimeType
|
657
|
+
file_def.storageId = _util.selector_for_latest(storage_id)
|
658
|
+
file_def.dataItem = data_item
|
659
|
+
file_def.size = 0
|
660
|
+
|
661
|
+
return file_def
|
662
|
+
|
663
|
+
@classmethod
|
664
|
+
def build_storage_def(
|
665
|
+
cls, data_item: str,
|
666
|
+
storage_key, storage_path, storage_format,
|
667
|
+
timestamp: _dt.datetime):
|
668
|
+
|
669
|
+
first_incarnation = 0
|
670
|
+
|
671
|
+
storage_copy = meta.StorageCopy(
|
672
|
+
storage_key, storage_path, storage_format,
|
673
|
+
copyStatus=meta.CopyStatus.COPY_AVAILABLE,
|
674
|
+
copyTimestamp=meta.DatetimeValue(timestamp.isoformat()))
|
675
|
+
|
676
|
+
storage_incarnation = meta.StorageIncarnation(
|
677
|
+
[storage_copy],
|
678
|
+
incarnationIndex=first_incarnation,
|
679
|
+
incarnationTimestamp=meta.DatetimeValue(timestamp.isoformat()),
|
680
|
+
incarnationStatus=meta.IncarnationStatus.INCARNATION_AVAILABLE)
|
681
|
+
|
682
|
+
storage_item = meta.StorageItem([storage_incarnation])
|
683
|
+
|
684
|
+
storage_def = meta.StorageDefinition()
|
685
|
+
storage_def.dataItems[data_item] = storage_item
|
686
|
+
|
687
|
+
return storage_def
|
688
|
+
|
566
689
|
def build_job_results(
|
567
690
|
self,
|
568
691
|
objects: tp.Dict[str, NodeId[meta.ObjectDefinition]] = None,
|
tracdap/rt/_exec/runtime.py
CHANGED
@@ -96,6 +96,7 @@ class TracRuntime:
|
|
96
96
|
self._scratch_dir_persist = scratch_dir_persist
|
97
97
|
self._plugin_packages = plugin_packages or []
|
98
98
|
self._dev_mode = dev_mode
|
99
|
+
self._dev_mode_translator = None
|
99
100
|
|
100
101
|
# Runtime control
|
101
102
|
self._runtime_lock = threading.Lock()
|
@@ -141,10 +142,6 @@ class TracRuntime:
|
|
141
142
|
|
142
143
|
self._log.info(f"Beginning pre-start sequence...")
|
143
144
|
|
144
|
-
# Scratch dir is needed during pre-start (at least dev mode translation uses the model loader)
|
145
|
-
|
146
|
-
self._prepare_scratch_dir()
|
147
|
-
|
148
145
|
# Plugin manager, static API and guard rails are singletons
|
149
146
|
# Calling these methods multiple times is safe (e.g. for embedded or testing scenarios)
|
150
147
|
# However, plugins are never un-registered for the lifetime of the processes
|
@@ -198,9 +195,17 @@ class TracRuntime:
|
|
198
195
|
|
199
196
|
self._log.info("Starting the engine...")
|
200
197
|
|
198
|
+
self._prepare_scratch_dir()
|
199
|
+
|
201
200
|
self._models = _models.ModelLoader(self._sys_config, self._scratch_dir)
|
202
201
|
self._storage = _storage.StorageManager(self._sys_config)
|
203
202
|
|
203
|
+
if self._dev_mode:
|
204
|
+
|
205
|
+
self._dev_mode_translator = _dev_mode.DevModeTranslator(
|
206
|
+
self._sys_config, self._config_mgr, self._scratch_dir,
|
207
|
+
model_loader=self._models, storage_manager=self._storage)
|
208
|
+
|
204
209
|
# Enable protection after the initial setup of the runtime is complete
|
205
210
|
# Storage plugins in particular are likely to tigger protected imports
|
206
211
|
# Once the runtime is up, no more plugins should be loaded
|
@@ -323,6 +328,9 @@ class TracRuntime:
|
|
323
328
|
self, job_config: tp.Union[str, pathlib.Path, _cfg.JobConfig],
|
324
329
|
model_class: tp.Optional[_api.TracModel.__class__] = None):
|
325
330
|
|
331
|
+
if not self._engine or self._shutdown_requested:
|
332
|
+
raise _ex.ETracInternal("Engine is not started or shutdown has been requested")
|
333
|
+
|
326
334
|
if isinstance(job_config, _cfg.JobConfig):
|
327
335
|
self._log.info("Using embedded job config")
|
328
336
|
|
@@ -334,13 +342,15 @@ class TracRuntime:
|
|
334
342
|
config_file_name="job")
|
335
343
|
|
336
344
|
if self._dev_mode:
|
337
|
-
|
338
|
-
job_config = translator.translate_job_config(job_config, model_class)
|
345
|
+
job_config = self._dev_mode_translator.translate_job_config(job_config, model_class)
|
339
346
|
|
340
347
|
return job_config
|
341
348
|
|
342
349
|
def submit_job(self, job_config: _cfg.JobConfig):
|
343
350
|
|
351
|
+
if not self._engine or self._shutdown_requested:
|
352
|
+
raise _ex.ETracInternal("Engine is not started or shutdown has been requested")
|
353
|
+
|
344
354
|
job_key = _util.object_key(job_config.jobId)
|
345
355
|
self._jobs[job_key] = _RuntimeJobInfo()
|
346
356
|
|
@@ -351,6 +361,9 @@ class TracRuntime:
|
|
351
361
|
|
352
362
|
def wait_for_job(self, job_id: _api.TagHeader):
|
353
363
|
|
364
|
+
if not self._engine or self._shutdown_requested:
|
365
|
+
raise _ex.ETracInternal("Engine is not started or shutdown has been requested")
|
366
|
+
|
354
367
|
job_key = _util.object_key(job_id)
|
355
368
|
|
356
369
|
if job_key not in self._jobs:
|