tracdap-runtime 0.7.0rc1__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.
Files changed (34) hide show
  1. tracdap/rt/_exec/context.py +140 -64
  2. tracdap/rt/_exec/dev_mode.py +144 -69
  3. tracdap/rt/_exec/engine.py +9 -7
  4. tracdap/rt/_exec/functions.py +95 -33
  5. tracdap/rt/_exec/graph.py +22 -15
  6. tracdap/rt/_exec/graph_builder.py +221 -98
  7. tracdap/rt/_exec/runtime.py +19 -6
  8. tracdap/rt/_impl/data.py +86 -13
  9. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.py +3 -1
  10. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.pyi +8 -0
  11. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +27 -25
  12. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -4
  13. tracdap/rt/_impl/models.py +9 -7
  14. tracdap/rt/_impl/static_api.py +53 -33
  15. tracdap/rt/_impl/util.py +1 -1
  16. tracdap/rt/_impl/validation.py +54 -28
  17. tracdap/rt/_version.py +1 -1
  18. tracdap/rt/api/__init__.py +6 -3
  19. tracdap/rt/api/file_types.py +29 -0
  20. tracdap/rt/api/hook.py +15 -7
  21. tracdap/rt/api/model_api.py +16 -0
  22. tracdap/rt/api/static_api.py +211 -125
  23. tracdap/rt/config/__init__.py +6 -6
  24. tracdap/rt/config/common.py +11 -1
  25. tracdap/rt/config/platform.py +4 -6
  26. tracdap/rt/launch/launch.py +9 -11
  27. tracdap/rt/metadata/__init__.py +10 -9
  28. tracdap/rt/metadata/file.py +8 -0
  29. tracdap/rt/metadata/model.py +12 -2
  30. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b1.dist-info}/METADATA +15 -15
  31. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b1.dist-info}/RECORD +34 -33
  32. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b1.dist-info}/WHEEL +1 -1
  33. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b1.dist-info}/LICENSE +0 -0
  34. {tracdap_runtime-0.7.0rc1.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
- data_item_id: NodeId[_data.DataItem]
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
- storage_key: str
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
- return {self.spec_id: DependencyType.HARD}
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[None]):
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
- return {self.spec_id: DependencyType.HARD, self.data_item_id: DependencyType.HARD}
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, input_schema in required_inputs.items():
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
- data_selector = supplied_inputs.get(input_name)
371
+ if input_selector is None:
363
372
 
364
- if data_selector is None:
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
- nodes[data_view_id] = StaticValueNode(data_view_id, _data.DataView.create_empty())
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
- # Build a data spec using metadata from the job config
375
- # For now we are always loading the root part, snap 0, delta 0
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
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
- schema_def = data_def.schema
388
+ self._error(_ex.EJobValidation(f"Invalid input type [{input_type.name}] for input [{input_name}]"))
383
389
 
384
- root_part_opaque_key = 'part-root' # TODO: Central part names / constants
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
- # Data spec node is static, using the assembled data spec
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
- # Physical load of data items from disk
393
- # Currently one item per input, since inputs are single part/delta
394
- data_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
395
- data_load_node = LoadDataNode(data_load_id, data_spec_id, explicit_deps=explicit_deps)
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
- # Input views assembled by mapping one root part to each view
398
- data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
399
- data_view_node = DataViewNode(data_view_id, schema_def, data_load_id)
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
- nodes[data_spec_id] = data_spec_node
402
- nodes[data_load_id] = data_load_node
403
- nodes[data_view_id] = data_view_node
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
- # Job-level data view is an output of the load operation
406
- outputs.add(data_view_id)
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
- return GraphSection(nodes, outputs=outputs, must_run=must_run)
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, output_schema in required_outputs.items():
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
- data_selector = supplied_outputs.get(output_name)
453
+ output_selector = supplied_outputs.get(output_name)
424
454
 
425
- if data_selector is None:
426
- if output_schema.optional:
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
- # Output data view must already exist in the namespace
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)
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
- data_obj = _util.get_job_resource(data_selector, self._job_config, optional=True)
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
- if data_obj is not None:
470
+ else:
471
+ self._error(_ex.EJobValidation(f"Invalid output type [{output_type.name}] for input [{output_name}]"))
441
472
 
442
- # If data def for the output has been built in advance, use a static data spec
473
+ return GraphSection(nodes, inputs=inputs)
443
474
 
444
- data_def = data_obj.data
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
- if data_def.schemaId:
448
- schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
449
- else:
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
- root_part_opaque_key = 'part-root' # TODO: Central part names / constants
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
- data_spec_node = StaticValueNode(data_spec_id, data_spec, explicit_deps=explicit_deps)
483
+ if data_obj is not None:
457
484
 
458
- output_data_key = output_name + ":DATA"
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
- # If output data def for an output was not supplied in the job, create a dynamic data spec
464
- # Dynamic data def will always use an embedded schema (this is no ID for an external schema)
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
- data_key = output_name + ":DATA"
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
- data_spec_node = DynamicDataSpecNode(
472
- data_spec_id, data_view_id,
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
- output_data_key = _util.object_key(data_id)
478
- output_storage_key = _util.object_key(storage_id)
511
+ mapped_output_key = output_name
512
+ mapped_storage_key = output_name + ":STORAGE"
479
513
 
480
- # Map one data item from each view, since outputs are single part/delta
481
- data_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
482
- data_item_node = DataItemNode(data_item_id, data_view_id)
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, None)
486
- data_save_node = SaveDataNode(data_save_id, data_spec_id, data_item_id)
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
- data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
489
- data_result_node = DataResultNode(
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
- nodes[data_spec_id] = data_spec_node
495
- nodes[data_item_id] = data_item_node
496
- nodes[data_save_id] = data_save_node
497
- nodes[data_result_id] = data_result_node
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
- # Job-level data view is an input to the save operation
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
- return GraphSection(nodes, inputs=inputs)
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
- data_key = output_name + ":DATA"
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
- output_data_key = _util.object_key(data_id)
533
- output_storage_key = _util.object_key(storage_id)
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, None)
541
- data_save_node = SaveDataNode(data_save_id, data_spec_id, data_item_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
- data_item_id, data_spec_id, data_save_id,
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,
@@ -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
- translator = _dev_mode.DevModeTranslator(self._sys_config, self._config_mgr, self._scratch_dir)
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: