tracdap-runtime 0.7.0rc1__py3-none-any.whl → 0.8.0b2__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 (52) hide show
  1. tracdap/rt/_exec/actors.py +5 -4
  2. tracdap/rt/_exec/context.py +166 -74
  3. tracdap/rt/_exec/dev_mode.py +147 -71
  4. tracdap/rt/_exec/engine.py +224 -99
  5. tracdap/rt/_exec/functions.py +122 -80
  6. tracdap/rt/_exec/graph.py +23 -35
  7. tracdap/rt/_exec/graph_builder.py +250 -113
  8. tracdap/rt/_exec/runtime.py +24 -10
  9. tracdap/rt/_exec/server.py +4 -3
  10. tracdap/rt/_impl/config_parser.py +3 -2
  11. tracdap/rt/_impl/data.py +89 -16
  12. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.py +3 -1
  13. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.pyi +8 -0
  14. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +64 -62
  15. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +16 -2
  16. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +27 -25
  17. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -4
  18. tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.py +3 -3
  19. tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.pyi +2 -0
  20. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.py +4 -4
  21. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.pyi +4 -2
  22. tracdap/rt/_impl/logging.py +195 -0
  23. tracdap/rt/_impl/models.py +11 -8
  24. tracdap/rt/_impl/repos.py +5 -3
  25. tracdap/rt/_impl/schemas.py +2 -2
  26. tracdap/rt/_impl/shim.py +3 -2
  27. tracdap/rt/_impl/static_api.py +53 -33
  28. tracdap/rt/_impl/storage.py +4 -3
  29. tracdap/rt/_impl/util.py +1 -111
  30. tracdap/rt/_impl/validation.py +57 -30
  31. tracdap/rt/_version.py +1 -1
  32. tracdap/rt/api/__init__.py +6 -3
  33. tracdap/rt/api/file_types.py +29 -0
  34. tracdap/rt/api/hook.py +15 -7
  35. tracdap/rt/api/model_api.py +16 -0
  36. tracdap/rt/api/static_api.py +211 -125
  37. tracdap/rt/config/__init__.py +6 -6
  38. tracdap/rt/config/common.py +11 -1
  39. tracdap/rt/config/platform.py +4 -6
  40. tracdap/rt/ext/plugins.py +2 -2
  41. tracdap/rt/launch/launch.py +9 -11
  42. tracdap/rt/metadata/__init__.py +11 -9
  43. tracdap/rt/metadata/file.py +8 -0
  44. tracdap/rt/metadata/job.py +16 -0
  45. tracdap/rt/metadata/model.py +12 -2
  46. tracdap/rt/metadata/object.py +2 -0
  47. tracdap/rt/metadata/object_id.py +2 -0
  48. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b2.dist-info}/METADATA +15 -15
  49. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b2.dist-info}/RECORD +52 -50
  50. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b2.dist-info}/WHEEL +1 -1
  51. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b2.dist-info}/LICENSE +0 -0
  52. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0b2.dist-info}/top_level.txt +0 -0
@@ -13,10 +13,10 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
- from __future__ import annotations
17
-
16
+ import copy
18
17
  import datetime
19
18
  import abc
19
+ import pathlib
20
20
  import random
21
21
  import dataclasses as dc # noqa
22
22
 
@@ -28,6 +28,7 @@ import tracdap.rt._exec.graph_builder as _graph
28
28
  import tracdap.rt._impl.config_parser as _cfg_p # noqa
29
29
  import tracdap.rt._impl.type_system as _types # noqa
30
30
  import tracdap.rt._impl.data as _data # noqa
31
+ import tracdap.rt._impl.logging as _logging # noqa
31
32
  import tracdap.rt._impl.storage as _storage # noqa
32
33
  import tracdap.rt._impl.models as _models # noqa
33
34
  import tracdap.rt._impl.util as _util # noqa
@@ -227,11 +228,22 @@ class BuildJobResultFunc(NodeFunction[_config.JobResult]):
227
228
  job_result.jobId = self.node.job_id
228
229
  job_result.statusCode = meta.JobStatusCode.SUCCEEDED
229
230
 
231
+ if self.node.result_id is not None:
232
+
233
+ result_def = meta.ResultDefinition()
234
+ result_def.jobId = _util.selector_for(self.node.job_id)
235
+ result_def.statusCode = meta.JobStatusCode.SUCCEEDED
236
+
237
+ result_key = _util.object_key(self.node.result_id)
238
+ result_obj = meta.ObjectDefinition(objectType=meta.ObjectType.RESULT, result=result_def)
239
+
240
+ job_result.results[result_key] = result_obj
241
+
230
242
  # TODO: Handle individual failed results
231
243
 
232
- for obj_id, node_id in self.node.outputs.objects.items():
244
+ for obj_key, node_id in self.node.outputs.objects.items():
233
245
  obj_def = _ctx_lookup(node_id, ctx)
234
- job_result.results[obj_id] = obj_def
246
+ job_result.results[obj_key] = obj_def
235
247
 
236
248
  for bundle_id in self.node.outputs.bundles:
237
249
  bundle = _ctx_lookup(bundle_id, ctx)
@@ -241,9 +253,9 @@ class BuildJobResultFunc(NodeFunction[_config.JobResult]):
241
253
 
242
254
  runtime_outputs = _ctx_lookup(self.node.runtime_outputs, ctx)
243
255
 
244
- for obj_id, node_id in runtime_outputs.objects.items():
256
+ for obj_key, node_id in runtime_outputs.objects.items():
245
257
  obj_def = _ctx_lookup(node_id, ctx)
246
- job_result.results[obj_id] = obj_def
258
+ job_result.results[obj_key] = obj_def
247
259
 
248
260
  for bundle_id in runtime_outputs.bundles:
249
261
  bundle = _ctx_lookup(bundle_id, ctx)
@@ -252,37 +264,6 @@ class BuildJobResultFunc(NodeFunction[_config.JobResult]):
252
264
  return job_result
253
265
 
254
266
 
255
- class SaveJobResultFunc(NodeFunction[None]):
256
-
257
- def __init__(self, node: SaveJobResultNode):
258
- super().__init__()
259
- self.node = node
260
-
261
- def _execute(self, ctx: NodeContext) -> None:
262
-
263
- job_result = _ctx_lookup(self.node.job_result_id, ctx)
264
-
265
- if not self.node.result_spec.save_result:
266
- return None
267
-
268
- job_result_format = self.node.result_spec.result_format
269
- job_result_str = _cfg_p.ConfigQuoter.quote(job_result, job_result_format)
270
- job_result_bytes = bytes(job_result_str, "utf-8")
271
-
272
- job_key = _util.object_key(job_result.jobId)
273
- job_result_file = f"job_result_{job_key}.{self.node.result_spec.result_format}"
274
- job_result_path = pathlib \
275
- .Path(self.node.result_spec.result_dir) \
276
- .joinpath(job_result_file)
277
-
278
- _util.logger_for_object(self).info(f"Saving job result to [{job_result_path}]")
279
-
280
- with open(job_result_path, "xb") as result_stream:
281
- result_stream.write(job_result_bytes)
282
-
283
- return None
284
-
285
-
286
267
  class DataViewFunc(NodeFunction[_data.DataView]):
287
268
 
288
269
  def __init__(self, node: DataViewNode):
@@ -296,8 +277,13 @@ class DataViewFunc(NodeFunction[_data.DataView]):
296
277
 
297
278
  # Map empty item -> emtpy view (for optional inputs not supplied)
298
279
  if root_item.is_empty():
299
- return _data.DataView.create_empty()
280
+ return _data.DataView.create_empty(root_item.object_type)
300
281
 
282
+ # Handle file data views
283
+ if root_item.object_type == meta.ObjectType.FILE:
284
+ return _data.DataView.for_file_item(root_item)
285
+
286
+ # Everything else is a regular data view
301
287
  if self.node.schema is not None and len(self.node.schema.table.fields) > 0:
302
288
  trac_schema = self.node.schema
303
289
  else:
@@ -322,7 +308,11 @@ class DataItemFunc(NodeFunction[_data.DataItem]):
322
308
 
323
309
  # Map empty view -> emtpy item (for optional outputs not supplied)
324
310
  if data_view.is_empty():
325
- return _data.DataItem.create_empty()
311
+ return _data.DataItem.create_empty(data_view.object_type)
312
+
313
+ # Handle file data views
314
+ if data_view.object_type == meta.ObjectType.FILE:
315
+ return data_view.file_item
326
316
 
327
317
  # TODO: Support selecting data item described by self.node
328
318
 
@@ -342,25 +332,24 @@ class DataResultFunc(NodeFunction[ObjectBundle]):
342
332
 
343
333
  def _execute(self, ctx: NodeContext) -> ObjectBundle:
344
334
 
345
- data_item = _ctx_lookup(self.node.data_item_id, ctx)
335
+ data_spec = _ctx_lookup(self.node.data_save_id, ctx)
346
336
 
347
- # Do not record output metadata for optional outputs that are empty
348
- if data_item.is_empty():
349
- return {}
337
+ result_bundle = dict()
350
338
 
351
- data_spec = _ctx_lookup(self.node.data_spec_id, ctx)
339
+ # Do not record output metadata for optional outputs that are empty
340
+ if data_spec.is_empty():
341
+ return result_bundle
352
342
 
353
- # TODO: Check result of save operation
354
- # save_result = _ctx_lookup(self.node.data_save_id, ctx)
343
+ if self.node.data_key is not None:
344
+ result_bundle[self.node.data_key] = meta.ObjectDefinition(objectType=meta.ObjectType.DATA, data=data_spec.data_def)
355
345
 
356
- data_result = meta.ObjectDefinition(objectType=meta.ObjectType.DATA, data=data_spec.data_def)
357
- storage_result = meta.ObjectDefinition(objectType=meta.ObjectType.STORAGE, storage=data_spec.storage_def)
346
+ if self.node.file_key is not None:
347
+ result_bundle[self.node.file_key] = meta.ObjectDefinition(objectType=meta.ObjectType.FILE, file=data_spec.file_def)
358
348
 
359
- bundle = {
360
- self.node.data_key: data_result,
361
- self.node.storage_key: storage_result}
349
+ if self.node.storage_key is not None:
350
+ result_bundle[self.node.storage_key] = meta.ObjectDefinition(objectType=meta.ObjectType.STORAGE, storage=data_spec.storage_def)
362
351
 
363
- return bundle
352
+ return result_bundle
364
353
 
365
354
 
366
355
  class DynamicDataSpecFunc(NodeFunction[_data.DataSpec]):
@@ -443,11 +432,7 @@ class DynamicDataSpecFunc(NodeFunction[_data.DataSpec]):
443
432
 
444
433
  # Dynamic data def will always use an embedded schema (this is no ID for an external schema)
445
434
 
446
- return _data.DataSpec(
447
- data_item,
448
- data_def,
449
- storage_def,
450
- schema_def=None)
435
+ return _data.DataSpec.create_data_spec(data_item, data_def, storage_def, schema_def=None)
451
436
 
452
437
 
453
438
  class _LoadSaveDataFunc(abc.ABC):
@@ -455,6 +440,16 @@ class _LoadSaveDataFunc(abc.ABC):
455
440
  def __init__(self, storage: _storage.StorageManager):
456
441
  self.storage = storage
457
442
 
443
+ @classmethod
444
+ def _choose_data_spec(cls, spec_id, spec, ctx: NodeContext):
445
+
446
+ if spec_id is not None:
447
+ return _ctx_lookup(spec_id, ctx)
448
+ elif spec is not None:
449
+ return spec
450
+ else:
451
+ raise _ex.EUnexpected()
452
+
458
453
  def _choose_copy(self, data_item: str, storage_def: meta.StorageDefinition) -> meta.StorageCopy:
459
454
 
460
455
  # Metadata should be checked for consistency before a job is accepted
@@ -491,9 +486,19 @@ class LoadDataFunc( _LoadSaveDataFunc, NodeFunction[_data.DataItem],):
491
486
 
492
487
  def _execute(self, ctx: NodeContext) -> _data.DataItem:
493
488
 
494
- data_spec = _ctx_lookup(self.node.spec_id, ctx)
489
+ data_spec = self._choose_data_spec(self.node.spec_id, self.node.spec, ctx)
495
490
  data_copy = self._choose_copy(data_spec.data_item, data_spec.storage_def)
496
- data_storage = self.storage.get_data_storage(data_copy.storageKey)
491
+
492
+ if data_spec.object_type == _api.ObjectType.DATA:
493
+ return self._load_data(data_spec, data_copy)
494
+
495
+ elif data_spec.object_type == _api.ObjectType.FILE:
496
+ return self._load_file(data_copy)
497
+
498
+ else:
499
+ raise _ex.EUnexpected()
500
+
501
+ def _load_data(self, data_spec, data_copy):
497
502
 
498
503
  trac_schema = data_spec.schema_def if data_spec.schema_def else data_spec.data_def.schema
499
504
  arrow_schema = _data.DataMapping.trac_to_arrow_schema(trac_schema) if trac_schema else None
@@ -503,36 +508,52 @@ class LoadDataFunc( _LoadSaveDataFunc, NodeFunction[_data.DataItem],):
503
508
  for opt_key, opt_value in data_spec.storage_def.storageOptions.items():
504
509
  options[opt_key] = _types.MetadataCodec.decode_value(opt_value)
505
510
 
506
- table = data_storage.read_table(
511
+ storage = self.storage.get_data_storage(data_copy.storageKey)
512
+ table = storage.read_table(
507
513
  data_copy.storagePath,
508
514
  data_copy.storageFormat,
509
515
  arrow_schema,
510
516
  storage_options=options)
511
517
 
512
- return _data.DataItem(table.schema, table)
518
+ return _data.DataItem(_api.ObjectType.DATA, table.schema, table)
513
519
 
520
+ def _load_file(self, data_copy):
514
521
 
515
- class SaveDataFunc(_LoadSaveDataFunc, NodeFunction[None]):
522
+ storage = self.storage.get_file_storage(data_copy.storageKey)
523
+ raw_bytes = storage.read_bytes(data_copy.storagePath)
524
+
525
+ return _data.DataItem(_api.ObjectType.FILE, raw_bytes=raw_bytes)
526
+
527
+
528
+ class SaveDataFunc(_LoadSaveDataFunc, NodeFunction[_data.DataSpec]):
516
529
 
517
530
  def __init__(self, node: SaveDataNode, storage: _storage.StorageManager):
518
531
  super().__init__(storage)
519
532
  self.node = node
520
533
 
521
- def _execute(self, ctx: NodeContext):
534
+ def _execute(self, ctx: NodeContext) -> _data.DataSpec:
522
535
 
523
536
  # Item to be saved should exist in the current context
524
537
  data_item = _ctx_lookup(self.node.data_item_id, ctx)
525
538
 
539
+ # Metadata already exists as data_spec but may not contain schema, row count, file size etc.
540
+ data_spec = self._choose_data_spec(self.node.spec_id, self.node.spec, ctx)
541
+ data_copy = self._choose_copy(data_spec.data_item, data_spec.storage_def)
542
+
526
543
  # Do not save empty outputs (optional outputs that were not produced)
527
544
  if data_item.is_empty():
528
- return
545
+ return _data.DataSpec.create_empty_spec(data_item.object_type)
529
546
 
530
- # This function assumes that metadata has already been generated as the data_spec
531
- # i.e. it is already known which incarnation / copy of the data will be created
547
+ if data_item.object_type == _api.ObjectType.DATA:
548
+ return self._save_data(data_item, data_spec, data_copy)
532
549
 
533
- data_spec = _ctx_lookup(self.node.spec_id, ctx)
534
- data_copy = self._choose_copy(data_spec.data_item, data_spec.storage_def)
535
- data_storage = self.storage.get_data_storage(data_copy.storageKey)
550
+ elif data_item.object_type == _api.ObjectType.FILE:
551
+ return self._save_file(data_item, data_spec, data_copy)
552
+
553
+ else:
554
+ raise _ex.EUnexpected()
555
+
556
+ def _save_data(self, data_item, data_spec, data_copy):
536
557
 
537
558
  # Current implementation will always put an Arrow table in the data item
538
559
  # Empty tables are allowed, so explicitly check if table is None
@@ -546,11 +567,32 @@ class SaveDataFunc(_LoadSaveDataFunc, NodeFunction[None]):
546
567
  for opt_key, opt_value in data_spec.storage_def.storageOptions.items():
547
568
  options[opt_key] = _types.MetadataCodec.decode_value(opt_value)
548
569
 
549
- data_storage.write_table(
570
+ storage = self.storage.get_data_storage(data_copy.storageKey)
571
+ storage.write_table(
550
572
  data_copy.storagePath, data_copy.storageFormat,
551
573
  data_item.table,
552
574
  storage_options=options, overwrite=False)
553
575
 
576
+ data_spec = copy.deepcopy(data_spec)
577
+ # TODO: Save row count in metadata
578
+
579
+ if data_spec.data_def.schema is None and data_spec.data_def.schemaId is None:
580
+ data_spec.data_def.schema = _data.DataMapping.arrow_to_trac_schema(data_item.table.schema)
581
+
582
+ return data_spec
583
+
584
+ def _save_file(self, data_item, data_spec, data_copy):
585
+
586
+ if data_item.raw_bytes is None:
587
+ raise _ex.EUnexpected()
588
+
589
+ storage = self.storage.get_file_storage(data_copy.storageKey)
590
+ storage.write_bytes(data_copy.storagePath, data_item.raw_bytes)
591
+
592
+ data_spec = copy.deepcopy(data_spec)
593
+ data_spec.file_def.size = len(data_item.raw_bytes)
594
+
595
+ return data_spec
554
596
 
555
597
  def _model_def_for_import(import_details: meta.ImportModelJob):
556
598
 
@@ -571,8 +613,6 @@ class ImportModelFunc(NodeFunction[meta.ObjectDefinition]):
571
613
  self.node = node
572
614
  self._models = models
573
615
 
574
- self._log = _util.logger_for_object(self)
575
-
576
616
  def _execute(self, ctx: NodeContext) -> meta.ObjectDefinition:
577
617
 
578
618
  model_stub = _model_def_for_import(self.node.import_details)
@@ -589,13 +629,15 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
589
629
  self, node: RunModelNode,
590
630
  model_class: _api.TracModel.__class__,
591
631
  checkout_directory: pathlib.Path,
592
- storage_manager: _storage.StorageManager):
632
+ storage_manager: _storage.StorageManager,
633
+ log_provider: _logging.LogProvider):
593
634
 
594
635
  super().__init__()
595
636
  self.node = node
596
637
  self.model_class = model_class
597
638
  self.checkout_directory = checkout_directory
598
639
  self.storage_manager = storage_manager
640
+ self.log_provider = log_provider
599
641
 
600
642
  def _execute(self, ctx: NodeContext) -> Bundle[_data.DataView]:
601
643
 
@@ -622,7 +664,7 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
622
664
  for storage_key in self.node.storage_access:
623
665
  if self.storage_manager.has_file_storage(storage_key, external=True):
624
666
  storage_impl = self.storage_manager.get_file_storage(storage_key, external=True)
625
- storage = _ctx.TracFileStorageImpl(storage_key, storage_impl, write_access, self.checkout_directory)
667
+ storage = _ctx.TracFileStorageImpl(storage_key, storage_impl, write_access, self.checkout_directory, self.log_provider)
626
668
  storage_map[storage_key] = storage
627
669
  elif self.storage_manager.has_data_storage(storage_key, external=True):
628
670
  storage_impl = self.storage_manager.get_data_storage(storage_key, external=True)
@@ -630,7 +672,7 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
630
672
  if not isinstance(storage_impl, _storage.IDataStorageBase):
631
673
  raise _ex.EStorageConfig(f"External storage for [{storage_key}] is using the legacy storage framework]")
632
674
  converter = _data.DataConverter.noop()
633
- storage = _ctx.TracDataStorageImpl(storage_key, storage_impl, converter, write_access, self.checkout_directory)
675
+ storage = _ctx.TracDataStorageImpl(storage_key, storage_impl, converter, write_access, self.checkout_directory, self.log_provider)
634
676
  storage_map[storage_key] = storage
635
677
  else:
636
678
  raise _ex.EStorageConfig(f"External storage is not available: [{storage_key}]")
@@ -642,12 +684,12 @@ class RunModelFunc(NodeFunction[Bundle[_data.DataView]]):
642
684
  trac_ctx = _ctx.TracDataContextImpl(
643
685
  self.node.model_def, self.model_class,
644
686
  local_ctx, dynamic_outputs, storage_map,
645
- self.checkout_directory)
687
+ self.checkout_directory, self.log_provider)
646
688
  else:
647
689
  trac_ctx = _ctx.TracContextImpl(
648
690
  self.node.model_def, self.model_class,
649
691
  local_ctx, dynamic_outputs,
650
- self.checkout_directory)
692
+ self.checkout_directory, self.log_provider)
651
693
 
652
694
  try:
653
695
  model = self.model_class()
@@ -750,9 +792,10 @@ class FunctionResolver:
750
792
 
751
793
  __ResolveFunc = tp.Callable[['FunctionResolver', Node[_T]], NodeFunction[_T]]
752
794
 
753
- def __init__(self, models: _models.ModelLoader, storage: _storage.StorageManager):
795
+ def __init__(self, models: _models.ModelLoader, storage: _storage.StorageManager, log_provider: _logging.LogProvider):
754
796
  self._models = models
755
797
  self._storage = storage
798
+ self._log_provider = log_provider
756
799
 
757
800
  def resolve_node(self, node: Node[_T]) -> NodeFunction[_T]:
758
801
 
@@ -788,7 +831,7 @@ class FunctionResolver:
788
831
  checkout_directory = self._models.model_load_checkout_directory(node.model_scope, node.model_def)
789
832
  storage_manager = self._storage if node.storage_access else None
790
833
 
791
- return RunModelFunc(node, model_class, checkout_directory, storage_manager)
834
+ return RunModelFunc(node, model_class, checkout_directory, storage_manager, self._log_provider)
792
835
 
793
836
  __basic_node_mapping: tp.Dict[Node.__class__, NodeFunction.__class__] = {
794
837
 
@@ -799,7 +842,6 @@ class FunctionResolver:
799
842
  DataViewNode: DataViewFunc,
800
843
  DataItemNode: DataItemFunc,
801
844
  BuildJobResultNode: BuildJobResultFunc,
802
- SaveJobResultNode: SaveJobResultFunc,
803
845
  DataResultNode: DataResultFunc,
804
846
  StaticValueNode: StaticValueFunc,
805
847
  RuntimeOutputsNode: RuntimeOutputsFunc,
tracdap/rt/_exec/graph.py CHANGED
@@ -13,7 +13,6 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
- import pathlib
17
16
  import typing as tp
18
17
  import dataclasses as dc
19
18
 
@@ -182,15 +181,6 @@ class JobOutputs:
182
181
  bundles: tp.List[NodeId[ObjectBundle]] = dc.field(default_factory=list)
183
182
 
184
183
 
185
- # TODO: Where does this go?
186
- @dc.dataclass(frozen=True)
187
- class JobResultSpec:
188
-
189
- save_result: bool = False
190
- result_dir: tp.Union[str, pathlib.Path] = None
191
- result_format: str = None
192
-
193
-
194
184
  # ----------------------------------------------------------------------------------------------------------------------
195
185
  # NODE DEFINITIONS
196
186
  # ----------------------------------------------------------------------------------------------------------------------
@@ -309,20 +299,18 @@ class DataItemNode(MappingNode[_data.DataItem]):
309
299
  @_node_type
310
300
  class DataResultNode(Node[ObjectBundle]):
311
301
 
302
+ # TODO: Remove this node type
303
+ # Either produce metadata in SaveDataNode, or handle DataSpec outputs in result processing nodes
304
+
312
305
  output_name: str
313
- data_item_id: NodeId[_data.DataItem]
314
- data_spec_id: NodeId[_data.DataSpec]
315
- data_save_id: NodeId[type(None)]
306
+ data_save_id: NodeId[_data.DataSpec]
316
307
 
317
- data_key: str
318
- storage_key: str
308
+ data_key: str = None
309
+ file_key: str = None
310
+ storage_key: str = None
319
311
 
320
312
  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}
313
+ return {self.data_save_id: DependencyType.HARD}
326
314
 
327
315
 
328
316
  @_node_type
@@ -333,24 +321,33 @@ class LoadDataNode(Node[_data.DataItem]):
333
321
  The latest incarnation of the item will be loaded from any available copy
334
322
  """
335
323
 
336
- spec_id: NodeId[_data.DataSpec]
324
+ spec_id: tp.Optional[NodeId[_data.DataSpec]] = None
325
+ spec: tp.Optional[_data.DataSpec] = None
337
326
 
338
327
  def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
339
- return {self.spec_id: DependencyType.HARD}
328
+ deps = dict()
329
+ if self.spec_id is not None:
330
+ deps[self.spec_id] = DependencyType.HARD
331
+ return deps
340
332
 
341
333
 
342
334
  @_node_type
343
- class SaveDataNode(Node[None]):
335
+ class SaveDataNode(Node[_data.DataSpec]):
344
336
 
345
337
  """
346
338
  Save an individual data item to storage
347
339
  """
348
340
 
349
- spec_id: NodeId[_data.DataSpec]
350
341
  data_item_id: NodeId[_data.DataItem]
351
342
 
343
+ spec_id: tp.Optional[NodeId[_data.DataSpec]] = None
344
+ spec: tp.Optional[_data.DataSpec] = None
345
+
352
346
  def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
353
- return {self.spec_id: DependencyType.HARD, self.data_item_id: DependencyType.HARD}
347
+ deps = {self.data_item_id: DependencyType.HARD}
348
+ if self.spec_id is not None:
349
+ deps[self.spec_id] = DependencyType.HARD
350
+ return deps
354
351
 
355
352
 
356
353
  @_node_type
@@ -395,6 +392,7 @@ class RuntimeOutputsNode(Node[JobOutputs]):
395
392
  @_node_type
396
393
  class BuildJobResultNode(Node[cfg.JobResult]):
397
394
 
395
+ result_id: meta.TagHeader
398
396
  job_id: meta.TagHeader
399
397
 
400
398
  outputs: JobOutputs
@@ -407,16 +405,6 @@ class BuildJobResultNode(Node[cfg.JobResult]):
407
405
  return {node_id: DependencyType.HARD for node_id in dep_ids}
408
406
 
409
407
 
410
- @_node_type
411
- class SaveJobResultNode(Node[None]):
412
-
413
- job_result_id: NodeId[cfg.JobResult]
414
- result_spec: JobResultSpec
415
-
416
- def _node_dependencies(self) -> tp.Dict[NodeId, DependencyType]:
417
- return {self.job_result_id: DependencyType.HARD}
418
-
419
-
420
408
  @_node_type
421
409
  class ChildJobNode(Node[cfg.JobResult]):
422
410