tracdap-runtime 0.7.0rc1__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. tracdap/rt/_impl/core/__init__.py +14 -0
  2. tracdap/rt/_impl/{config_parser.py → core/config_parser.py} +61 -36
  3. tracdap/rt/_impl/{data.py → core/data.py} +136 -32
  4. tracdap/rt/_impl/core/logging.py +195 -0
  5. tracdap/rt/_impl/{models.py → core/models.py} +15 -12
  6. tracdap/rt/_impl/{repos.py → core/repos.py} +12 -3
  7. tracdap/rt/_impl/{schemas.py → core/schemas.py} +5 -5
  8. tracdap/rt/_impl/{shim.py → core/shim.py} +5 -4
  9. tracdap/rt/_impl/{storage.py → core/storage.py} +21 -10
  10. tracdap/rt/_impl/core/struct.py +547 -0
  11. tracdap/rt/_impl/{type_system.py → core/type_system.py} +73 -33
  12. tracdap/rt/_impl/{util.py → core/util.py} +1 -111
  13. tracdap/rt/_impl/{validation.py → core/validation.py} +99 -31
  14. tracdap/rt/_impl/exec/__init__.py +14 -0
  15. tracdap/rt/{_exec → _impl/exec}/actors.py +12 -14
  16. tracdap/rt/{_exec → _impl/exec}/context.py +228 -82
  17. tracdap/rt/{_exec → _impl/exec}/dev_mode.py +176 -89
  18. tracdap/rt/{_exec → _impl/exec}/engine.py +230 -105
  19. tracdap/rt/{_exec → _impl/exec}/functions.py +191 -100
  20. tracdap/rt/{_exec → _impl/exec}/graph.py +24 -36
  21. tracdap/rt/{_exec → _impl/exec}/graph_builder.py +252 -115
  22. tracdap/rt/_impl/grpc/codec.py +1 -1
  23. tracdap/rt/{_exec → _impl/grpc}/server.py +7 -6
  24. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.py +3 -3
  25. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2_grpc.py +1 -1
  26. tracdap/rt/_impl/grpc/tracdap/metadata/common_pb2.py +1 -1
  27. tracdap/rt/_impl/grpc/tracdap/metadata/config_pb2.py +40 -0
  28. tracdap/rt/_impl/grpc/tracdap/metadata/config_pb2.pyi +62 -0
  29. tracdap/rt/_impl/grpc/tracdap/metadata/custom_pb2.py +1 -1
  30. tracdap/rt/_impl/grpc/tracdap/metadata/data_pb2.py +32 -20
  31. tracdap/rt/_impl/grpc/tracdap/metadata/data_pb2.pyi +48 -2
  32. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.py +4 -2
  33. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.pyi +8 -0
  34. tracdap/rt/_impl/grpc/tracdap/metadata/flow_pb2.py +1 -1
  35. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +65 -63
  36. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +16 -2
  37. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +28 -26
  38. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -4
  39. tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.py +4 -4
  40. tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.pyi +6 -0
  41. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.py +9 -7
  42. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.pyi +12 -4
  43. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +18 -5
  44. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.pyi +42 -2
  45. tracdap/rt/_impl/grpc/tracdap/metadata/search_pb2.py +1 -1
  46. tracdap/rt/_impl/grpc/tracdap/metadata/{stoarge_pb2.py → storage_pb2.py} +4 -4
  47. tracdap/rt/_impl/grpc/tracdap/metadata/tag_pb2.py +1 -1
  48. tracdap/rt/_impl/grpc/tracdap/metadata/tag_update_pb2.py +1 -1
  49. tracdap/rt/_impl/grpc/tracdap/metadata/type_pb2.py +1 -1
  50. tracdap/rt/{_exec → _impl}/runtime.py +32 -18
  51. tracdap/rt/_impl/static_api.py +66 -38
  52. tracdap/rt/_plugins/format_csv.py +1 -1
  53. tracdap/rt/_plugins/repo_git.py +56 -11
  54. tracdap/rt/_plugins/storage_sql.py +13 -6
  55. tracdap/rt/_version.py +1 -1
  56. tracdap/rt/api/__init__.py +5 -24
  57. tracdap/rt/api/constants.py +57 -0
  58. tracdap/rt/api/experimental.py +32 -0
  59. tracdap/rt/api/hook.py +26 -7
  60. tracdap/rt/api/model_api.py +16 -0
  61. tracdap/rt/api/static_api.py +265 -127
  62. tracdap/rt/config/__init__.py +11 -11
  63. tracdap/rt/config/common.py +2 -26
  64. tracdap/rt/config/dynamic.py +28 -0
  65. tracdap/rt/config/platform.py +17 -31
  66. tracdap/rt/config/runtime.py +2 -0
  67. tracdap/rt/ext/embed.py +2 -2
  68. tracdap/rt/ext/plugins.py +3 -3
  69. tracdap/rt/launch/launch.py +12 -14
  70. tracdap/rt/metadata/__init__.py +31 -21
  71. tracdap/rt/metadata/config.py +95 -0
  72. tracdap/rt/metadata/data.py +40 -0
  73. tracdap/rt/metadata/file.py +10 -0
  74. tracdap/rt/metadata/job.py +16 -0
  75. tracdap/rt/metadata/model.py +12 -2
  76. tracdap/rt/metadata/object.py +9 -1
  77. tracdap/rt/metadata/object_id.py +6 -0
  78. tracdap/rt/metadata/resource.py +41 -1
  79. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0.dist-info}/METADATA +33 -27
  80. tracdap_runtime-0.8.0.dist-info/RECORD +129 -0
  81. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0.dist-info}/WHEEL +1 -1
  82. tracdap/rt/_exec/__init__.py +0 -0
  83. tracdap_runtime-0.7.0rc1.dist-info/RECORD +0 -121
  84. /tracdap/rt/_impl/{guard_rails.py → core/guard_rails.py} +0 -0
  85. /tracdap/rt/_impl/grpc/tracdap/metadata/{stoarge_pb2.pyi → storage_pb2.pyi} +0 -0
  86. /tracdap/rt/metadata/{stoarge.py → storage.py} +0 -0
  87. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0.dist-info/licenses}/LICENSE +0 -0
  88. {tracdap_runtime-0.7.0rc1.dist-info → tracdap_runtime-0.8.0.dist-info}/top_level.txt +0 -0
@@ -13,10 +13,12 @@
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
- import tracdap.rt._impl.data as _data # noqa
19
- import tracdap.rt._impl.util as _util # noqa
20
+ import tracdap.rt._impl.core.data as _data
21
+ import tracdap.rt._impl.core.util as _util
20
22
 
21
23
  from .graph import *
22
24
 
@@ -33,10 +35,10 @@ 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):
37
39
 
40
+ self._sys_config = sys_config
38
41
  self._job_config = job_config
39
- self._result_spec = result_spec
40
42
 
41
43
  self._job_key = _util.object_key(job_config.jobId)
42
44
  self._job_namespace = NodeNamespace(self._job_key)
@@ -45,7 +47,7 @@ class GraphBuilder:
45
47
 
46
48
  def _child_builder(self, job_id: meta.TagHeader) -> "GraphBuilder":
47
49
 
48
- builder = GraphBuilder(self._job_config, JobResultSpec(save_result=False))
50
+ builder = GraphBuilder(self._sys_config, self._job_config)
49
51
  builder._job_key = _util.object_key(job_id)
50
52
  builder._job_namespace = NodeNamespace(builder._job_key)
51
53
 
@@ -355,58 +357,76 @@ class GraphBuilder:
355
357
 
356
358
  nodes = dict()
357
359
  outputs = set()
358
- must_run = list()
359
360
 
360
- for input_name, input_schema in required_inputs.items():
361
+ for input_name, input_def in required_inputs.items():
362
+
363
+ # Backwards compatibility with pre 0.8 versions
364
+ input_type = meta.ObjectType.DATA \
365
+ if input_def.objectType == meta.ObjectType.OBJECT_TYPE_NOT_SET \
366
+ else input_def.objectType
367
+
368
+ input_selector = supplied_inputs.get(input_name)
361
369
 
362
- data_selector = supplied_inputs.get(input_name)
370
+ if input_selector is None:
363
371
 
364
- if data_selector is None:
365
- if input_schema.optional:
372
+ if input_def.optional:
366
373
  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())
374
+ data_view = _data.DataView.create_empty(input_type)
375
+ nodes[data_view_id] = StaticValueNode(data_view_id, data_view, explicit_deps=explicit_deps)
368
376
  outputs.add(data_view_id)
369
- continue
370
377
  else:
371
378
  self._error(_ex.EJobValidation(f"Missing required input: [{input_name}]"))
372
- continue
373
379
 
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
380
+ elif input_type == meta.ObjectType.DATA:
381
+ self._build_data_input(input_name, input_selector, nodes, outputs, explicit_deps)
382
+
383
+ elif input_type == meta.ObjectType.FILE:
384
+ self._build_file_input(input_name, input_selector, nodes, outputs, explicit_deps)
378
385
 
379
- if data_def.schemaId:
380
- schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
381
386
  else:
382
- schema_def = data_def.schema
387
+ self._error(_ex.EJobValidation(f"Invalid input type [{input_type.name}] for input [{input_name}]"))
383
388
 
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)
389
+ return GraphSection(nodes, outputs=outputs)
387
390
 
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)
391
+ def _build_data_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
391
392
 
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)
393
+ # Build a data spec using metadata from the job config
394
+ # For now we are always loading the root part, snap 0, delta 0
395
+ data_def = _util.get_job_resource(input_selector, self._job_config).data
396
+ storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
396
397
 
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)
398
+ if data_def.schemaId:
399
+ schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
400
+ else:
401
+ schema_def = data_def.schema
400
402
 
401
- nodes[data_spec_id] = data_spec_node
402
- nodes[data_load_id] = data_load_node
403
- nodes[data_view_id] = data_view_node
403
+ root_part_opaque_key = 'part-root' # TODO: Central part names / constants
404
+ data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
405
+ data_spec = _data.DataSpec.create_data_spec(data_item, data_def, storage_def, schema_def)
406
+
407
+ # Physical load of data items from disk
408
+ # Currently one item per input, since inputs are single part/delta
409
+ data_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
410
+ nodes[data_load_id] = LoadDataNode(data_load_id, spec=data_spec, explicit_deps=explicit_deps)
411
+
412
+ # Input views assembled by mapping one root part to each view
413
+ data_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
414
+ nodes[data_view_id] = DataViewNode(data_view_id, schema_def, data_load_id)
415
+ outputs.add(data_view_id)
416
+
417
+ def _build_file_input(self, input_name, input_selector, nodes, outputs, explicit_deps):
418
+
419
+ file_def = _util.get_job_resource(input_selector, self._job_config).file
420
+ storage_def = _util.get_job_resource(file_def.storageId, self._job_config).storage
404
421
 
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)
422
+ file_spec = _data.DataSpec.create_file_spec(file_def.dataItem, file_def, storage_def)
423
+ file_load_id = NodeId.of(f"{input_name}:LOAD", self._job_namespace, _data.DataItem)
424
+ nodes[file_load_id] = LoadDataNode(file_load_id, spec=file_spec, explicit_deps=explicit_deps)
408
425
 
409
- return GraphSection(nodes, outputs=outputs, must_run=must_run)
426
+ # Input views assembled by mapping one root part to each view
427
+ file_view_id = NodeId.of(input_name, self._job_namespace, _data.DataView)
428
+ nodes[file_view_id] = DataViewNode(file_view_id, None, file_load_id)
429
+ outputs.add(file_view_id)
410
430
 
411
431
  def build_job_outputs(
412
432
  self,
@@ -418,12 +438,21 @@ class GraphBuilder:
418
438
  nodes = {}
419
439
  inputs = set()
420
440
 
421
- for output_name, output_schema in required_outputs.items():
441
+ for output_name, output_def in required_outputs.items():
422
442
 
423
- data_selector = supplied_outputs.get(output_name)
443
+ # Output data view must already exist in the namespace, it is an input to the save operation
444
+ data_view_id = NodeId.of(output_name, self._job_namespace, _data.DataView)
445
+ inputs.add(data_view_id)
446
+
447
+ # Backwards compatibility with pre 0.8 versions
448
+ output_type = meta.ObjectType.DATA \
449
+ if output_def.objectType == meta.ObjectType.OBJECT_TYPE_NOT_SET \
450
+ else output_def.objectType
424
451
 
425
- if data_selector is None:
426
- if output_schema.optional:
452
+ output_selector = supplied_outputs.get(output_name)
453
+
454
+ if output_selector is None:
455
+ if output_def.optional:
427
456
  optional_info = "(configuration is required for all optional outputs, in case they are produced)"
428
457
  self._error(_ex.EJobValidation(f"Missing optional output: [{output_name}] {optional_info}"))
429
458
  continue
@@ -431,75 +460,150 @@ class GraphBuilder:
431
460
  self._error(_ex.EJobValidation(f"Missing required output: [{output_name}]"))
432
461
  continue
433
462
 
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)
463
+ elif output_type == meta.ObjectType.DATA:
464
+ self._build_data_output(output_name, output_selector, data_view_id, nodes, explicit_deps)
465
+
466
+ elif output_type == meta.ObjectType.FILE:
467
+ self._build_file_output(output_name, output_def, output_selector, data_view_id, nodes, explicit_deps)
437
468
 
438
- data_obj = _util.get_job_resource(data_selector, self._job_config, optional=True)
469
+ else:
470
+ self._error(_ex.EJobValidation(f"Invalid output type [{output_type.name}] for input [{output_name}]"))
439
471
 
440
- if data_obj is not None:
472
+ return GraphSection(nodes, inputs=inputs)
441
473
 
442
- # If data def for the output has been built in advance, use a static data spec
474
+ def _build_data_output(self, output_name, output_selector, data_view_id, nodes, explicit_deps):
443
475
 
444
- data_def = data_obj.data
445
- storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
476
+ # Map one data item from each view, since outputs are single part/delta
477
+ data_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
478
+ nodes[data_item_id] = DataItemNode(data_item_id, data_view_id)
446
479
 
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
480
+ data_obj = _util.get_job_resource(output_selector, self._job_config, optional=True)
451
481
 
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)
482
+ if data_obj is not None:
455
483
 
456
- data_spec_node = StaticValueNode(data_spec_id, data_spec, explicit_deps=explicit_deps)
484
+ # If data def for the output has been built in advance, use a static data spec
457
485
 
458
- output_data_key = output_name + ":DATA"
459
- output_storage_key = output_name + ":STORAGE"
486
+ data_def = data_obj.data
487
+ storage_def = _util.get_job_resource(data_def.storageId, self._job_config).storage
460
488
 
489
+ if data_def.schemaId:
490
+ schema_def = _util.get_job_resource(data_def.schemaId, self._job_config).schema
461
491
  else:
492
+ schema_def = data_def.schema
462
493
 
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)
494
+ root_part_opaque_key = 'part-root' # TODO: Central part names / constants
495
+ data_item = data_def.parts[root_part_opaque_key].snap.deltas[0].dataItem
496
+ data_spec = _data.DataSpec.create_data_spec(data_item, data_def, storage_def, schema_def)
465
497
 
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]
498
+ # Create a physical save operation for the data item
499
+ data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
500
+ nodes[data_save_id] = SaveDataNode(data_save_id, data_item_id, spec=data_spec)
470
501
 
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)
502
+ output_key = output_name
503
+ storage_key = output_name + ":STORAGE"
476
504
 
477
- output_data_key = _util.object_key(data_id)
478
- output_storage_key = _util.object_key(storage_id)
505
+ else:
479
506
 
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)
507
+ # If output data def for an output was not supplied in the job, create a dynamic data spec
508
+ # Dynamic data def will always use an embedded schema (this is no ID for an external schema)
509
+
510
+ mapped_output_key = output_name
511
+ mapped_storage_key = output_name + ":STORAGE"
512
+
513
+ data_id = self._job_config.resultMapping[mapped_output_key]
514
+ storage_id = self._job_config.resultMapping[mapped_storage_key]
515
+
516
+ data_spec_id = NodeId.of(f"{output_name}:SPEC", self._job_namespace, _data.DataSpec)
517
+ nodes[data_spec_id] = DynamicDataSpecNode(
518
+ data_spec_id, data_view_id,
519
+ data_id, storage_id,
520
+ prior_data_spec=None,
521
+ explicit_deps=explicit_deps)
483
522
 
484
523
  # 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)
524
+ data_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
525
+ nodes[data_save_id] = SaveDataNode(data_save_id, data_item_id, spec_id=data_spec_id)
487
526
 
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)
527
+ output_key = _util.object_key(data_id)
528
+ storage_key = _util.object_key(storage_id)
493
529
 
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
530
+ data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
531
+ nodes[data_result_id] = DataResultNode(
532
+ data_result_id, output_name, data_save_id,
533
+ data_key=output_key,
534
+ storage_key=storage_key)
498
535
 
499
- # Job-level data view is an input to the save operation
500
- inputs.add(data_view_id)
536
+ def _build_file_output(self, output_name, output_def, output_selector, file_view_id, nodes, explicit_deps):
501
537
 
502
- return GraphSection(nodes, inputs=inputs)
538
+ mapped_output_key = output_name
539
+ mapped_storage_key = output_name + ":STORAGE"
540
+
541
+ file_obj = _util.get_job_resource(output_selector, self._job_config, optional=True)
542
+
543
+ if file_obj is not None:
544
+
545
+ # Definitions already exist (generated by dev mode translator)
546
+
547
+ file_def = _util.get_job_resource(output_selector, self._job_config).file
548
+ storage_def = _util.get_job_resource(file_def.storageId, self._job_config).storage
549
+
550
+ resolved_output_key = mapped_output_key
551
+ resolved_storage_key = mapped_storage_key
552
+
553
+ else:
554
+
555
+ # Create new definitions (default behavior for jobs sent from the platform)
556
+
557
+ output_id = self._job_config.resultMapping[mapped_output_key]
558
+ storage_id = self._job_config.resultMapping[mapped_storage_key]
559
+
560
+ file_type = output_def.fileType
561
+ timestamp = _dt.datetime.fromisoformat(output_id.objectTimestamp.isoDatetime)
562
+ data_item = f"file/{output_id.objectId}/version-{output_id.objectVersion}"
563
+ storage_key = self._sys_config.storage.defaultBucket
564
+ storage_path = f"file/FILE-{output_id.objectId}/version-{output_id.objectVersion}/{output_name}.{file_type.extension}"
565
+
566
+ file_def = self.build_file_def(output_name, file_type, storage_id, data_item)
567
+ storage_def = self.build_storage_def(data_item, storage_key, storage_path, file_type.mimeType, timestamp)
568
+
569
+ resolved_output_key = _util.object_key(output_id)
570
+ resolved_storage_key = _util.object_key(storage_id)
571
+
572
+ # Required object defs are available, now build the graph nodes
573
+
574
+ file_item_id = NodeId(f"{output_name}:ITEM", self._job_namespace, _data.DataItem)
575
+ nodes[file_item_id] = DataItemNode(file_item_id, file_view_id, explicit_deps=explicit_deps)
576
+
577
+ file_spec = _data.DataSpec.create_file_spec(file_def.dataItem, file_def, storage_def)
578
+ file_save_id = NodeId.of(f"{output_name}:SAVE", self._job_namespace, _data.DataSpec)
579
+ nodes[file_save_id] = SaveDataNode(file_save_id, file_item_id, spec=file_spec)
580
+
581
+ data_result_id = NodeId.of(f"{output_name}:RESULT", self._job_namespace, ObjectBundle)
582
+ nodes[data_result_id] = DataResultNode(
583
+ data_result_id, output_name, file_save_id,
584
+ file_key=resolved_output_key,
585
+ storage_key=resolved_storage_key)
586
+
587
+ @classmethod
588
+ def build_output_file_and_storage(cls, output_key, file_type: meta.FileType, sys_config: cfg.RuntimeConfig, job_config: cfg.JobConfig):
589
+
590
+ # TODO: Review and de-dupe building of output metadata
591
+ # Responsibility for assigning outputs could perhaps move from orchestrator to runtime
592
+
593
+ output_storage_key = f"{output_key}:STORAGE"
594
+
595
+ output_id = job_config.resultMapping[output_key]
596
+ output_storage_id = job_config.resultMapping[output_storage_key]
597
+
598
+ timestamp = _dt.datetime.fromisoformat(output_id.objectTimestamp.isoDatetime)
599
+ data_item = f"file/{output_id.objectId}/version-{output_id.objectVersion}"
600
+ storage_key = sys_config.storage.defaultBucket
601
+ storage_path = f"file/FILE-{output_id.objectId}/version-{output_id.objectVersion}/{output_key}.{file_type.extension}"
602
+
603
+ file_def = cls.build_file_def(output_key, file_type, output_storage_id, data_item)
604
+ storage_def = cls.build_storage_def(data_item, storage_key, storage_path, file_type.mimeType, timestamp)
605
+
606
+ return file_def, storage_def
503
607
 
504
608
  @classmethod
505
609
  def build_runtime_outputs(cls, output_names: tp.List[str], job_namespace: NodeNamespace):
@@ -519,9 +623,10 @@ class GraphBuilder:
519
623
  data_view_id = NodeId.of(output_name, job_namespace, _data.DataView)
520
624
  data_spec_id = NodeId.of(f"{output_name}:SPEC", job_namespace, _data.DataSpec)
521
625
 
522
- data_key = output_name + ":DATA"
626
+ mapped_output_key = output_name
627
+ mapped_storage_key = output_name + ":STORAGE"
628
+
523
629
  data_id = _util.new_object_id(meta.ObjectType.DATA)
524
- storage_key = output_name + ":STORAGE"
525
630
  storage_id = _util.new_object_id(meta.ObjectType.STORAGE)
526
631
 
527
632
  data_spec_node = DynamicDataSpecNode(
@@ -529,22 +634,21 @@ class GraphBuilder:
529
634
  data_id, storage_id,
530
635
  prior_data_spec=None)
531
636
 
532
- output_data_key = _util.object_key(data_id)
533
- output_storage_key = _util.object_key(storage_id)
637
+ output_key = _util.object_key(data_id)
638
+ storage_key = _util.object_key(storage_id)
534
639
 
535
640
  # Map one data item from each view, since outputs are single part/delta
536
641
  data_item_id = NodeId(f"{output_name}:ITEM", job_namespace, _data.DataItem)
537
642
  data_item_node = DataItemNode(data_item_id, data_view_id)
538
643
 
539
644
  # 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)
645
+ data_save_id = NodeId.of(f"{output_name}:SAVE", job_namespace, _data.DataSpec)
646
+ data_save_node = SaveDataNode(data_save_id, data_item_id, spec_id=data_spec_id)
542
647
 
543
648
  data_result_id = NodeId.of(f"{output_name}:RESULT", job_namespace, ObjectBundle)
544
649
  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)
650
+ data_result_id, output_name, data_save_id,
651
+ output_key, storage_key)
548
652
 
549
653
  nodes[data_spec_id] = data_spec_node
550
654
  nodes[data_item_id] = data_item_node
@@ -563,6 +667,45 @@ class GraphBuilder:
563
667
 
564
668
  return GraphSection(nodes, inputs=inputs, outputs={runtime_outputs_id})
565
669
 
670
+ @classmethod
671
+ def build_file_def(cls, file_name, file_type, storage_id, data_item):
672
+
673
+ file_def = meta.FileDefinition()
674
+ file_def.name = f"{file_name}.{file_type.extension}"
675
+ file_def.extension = file_type.extension
676
+ file_def.mimeType = file_type.mimeType
677
+ file_def.storageId = _util.selector_for_latest(storage_id)
678
+ file_def.dataItem = data_item
679
+ file_def.size = 0
680
+
681
+ return file_def
682
+
683
+ @classmethod
684
+ def build_storage_def(
685
+ cls, data_item: str,
686
+ storage_key, storage_path, storage_format,
687
+ timestamp: _dt.datetime):
688
+
689
+ first_incarnation = 0
690
+
691
+ storage_copy = meta.StorageCopy(
692
+ storage_key, storage_path, storage_format,
693
+ copyStatus=meta.CopyStatus.COPY_AVAILABLE,
694
+ copyTimestamp=meta.DatetimeValue(timestamp.isoformat()))
695
+
696
+ storage_incarnation = meta.StorageIncarnation(
697
+ [storage_copy],
698
+ incarnationIndex=first_incarnation,
699
+ incarnationTimestamp=meta.DatetimeValue(timestamp.isoformat()),
700
+ incarnationStatus=meta.IncarnationStatus.INCARNATION_AVAILABLE)
701
+
702
+ storage_item = meta.StorageItem([storage_incarnation])
703
+
704
+ storage_def = meta.StorageDefinition()
705
+ storage_def.dataItems[data_item] = storage_item
706
+
707
+ return storage_def
708
+
566
709
  def build_job_results(
567
710
  self,
568
711
  objects: tp.Dict[str, NodeId[meta.ObjectDefinition]] = None,
@@ -570,15 +713,16 @@ class GraphBuilder:
570
713
  explicit_deps: tp.Optional[tp.List[NodeId]] = None) \
571
714
  -> GraphSection:
572
715
 
573
- build_result_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
716
+ result_id = self._job_config.resultMapping.get("trac_job_result")
717
+ result_node_id = NodeId.of("trac_job_result", self._job_namespace, cfg.JobResult)
574
718
 
575
719
  if objects is not None:
576
720
 
577
721
  results_inputs = set(objects.values())
578
722
 
579
723
  build_result_node = BuildJobResultNode(
580
- build_result_id, self._job_config.jobId,
581
- outputs = JobOutputs(objects=objects),
724
+ result_node_id, result_id, self._job_config.jobId,
725
+ outputs=JobOutputs(objects=objects),
582
726
  explicit_deps=explicit_deps)
583
727
 
584
728
  elif bundles is not None:
@@ -586,23 +730,16 @@ class GraphBuilder:
586
730
  results_inputs = set(bundles)
587
731
 
588
732
  build_result_node = BuildJobResultNode(
589
- build_result_id, self._job_config.jobId,
590
- outputs = JobOutputs(bundles=bundles),
733
+ result_node_id, result_id, self._job_config.jobId,
734
+ outputs=JobOutputs(bundles=bundles),
591
735
  explicit_deps=explicit_deps)
592
736
 
593
737
  else:
594
738
  raise _ex.EUnexpected()
595
739
 
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)
599
- result_nodes = {build_result_id: build_result_node, save_result_id: save_result_node}
600
- job_result_id = save_result_id
601
- else:
602
- result_nodes = {build_result_id: build_result_node}
603
- job_result_id = build_result_id
740
+ result_nodes = {result_node_id: build_result_node}
604
741
 
605
- return GraphSection(result_nodes, inputs=results_inputs, must_run=[job_result_id])
742
+ return GraphSection(result_nodes, inputs=results_inputs, must_run=[result_node_id])
606
743
 
607
744
  def build_model_or_flow_with_context(
608
745
  self, namespace: NodeNamespace, model_or_flow_name: str,
@@ -24,7 +24,7 @@ import tracdap.rt._impl.grpc.tracdap.metadata.object_id_pb2 as object_id_pb2
24
24
  import tracdap.rt._impl.grpc.tracdap.metadata.object_pb2 as object_pb2
25
25
  from tracdap.rt._impl.grpc.tracdap.metadata import model_pb2
26
26
  import tracdap.rt._impl.grpc.tracdap.metadata.data_pb2 as data_pb2
27
- import tracdap.rt._impl.grpc.tracdap.metadata.stoarge_pb2 as storage_pb2
27
+ import tracdap.rt._impl.grpc.tracdap.metadata.storage_pb2 as storage_pb2
28
28
 
29
29
  from google.protobuf import message as _message
30
30
 
@@ -19,9 +19,10 @@ import typing as tp
19
19
 
20
20
  import tracdap.rt.config as config
21
21
  import tracdap.rt.exceptions as ex
22
- import tracdap.rt._exec.actors as actors
23
- import tracdap.rt._impl.grpc.codec as codec # noqa
24
- import tracdap.rt._impl.util as util # noqa
22
+ import tracdap.rt._impl.exec.actors as actors
23
+ import tracdap.rt._impl.grpc.codec as codec
24
+ import tracdap.rt._impl.core.logging as logging
25
+ import tracdap.rt._impl.core.util as util
25
26
 
26
27
  # Check whether gRPC is installed before trying to load any of the generated modules
27
28
  try:
@@ -44,7 +45,7 @@ class RuntimeApiServer(runtime_grpc.TracRuntimeApiServicer):
44
45
 
45
46
  def __init__(self, system: actors.ActorSystem, port: int):
46
47
 
47
- self.__log = util.logger_for_object(self)
48
+ self.__log = logging.logger_for_object(self)
48
49
 
49
50
  self.__system = system
50
51
  self.__engine_id = system.main_id()
@@ -158,7 +159,7 @@ class ApiAgent(actors.ThreadsafeActor):
158
159
 
159
160
  def __init__(self):
160
161
  super().__init__()
161
- self._log = util.logger_for_object(self)
162
+ self._log = logging.logger_for_object(self)
162
163
  self._event_loop = asyncio.get_event_loop()
163
164
  self.__start_signal = asyncio.Event()
164
165
 
@@ -258,7 +259,7 @@ class ApiRequest(actors.ThreadsafeActor, tp.Generic[_T_REQUEST, _T_RESPONSE]):
258
259
  self.threadsafe().stop()
259
260
 
260
261
 
261
- ApiRequest._log = util.logger_for_class(ApiRequest)
262
+ ApiRequest._log = logging.logger_for_class(ApiRequest)
262
263
 
263
264
 
264
265
  class ListJobsRequest(ApiRequest[runtime_pb2.RuntimeListJobsRequest, runtime_pb2.RuntimeListJobsResponse]):
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # source: tracdap/rt/_impl/grpc/tracdap/api/internal/runtime.proto
4
- # Protobuf Python Version: 4.25.3
4
+ # Protobuf Python Version: 4.25.5
5
5
  """Generated protocol buffer code."""
6
6
  from google.protobuf import descriptor as _descriptor
7
7
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -18,14 +18,14 @@ from tracdap.rt._impl.grpc.tracdap.metadata import object_pb2 as tracdap_dot_rt_
18
18
  from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2
19
19
 
20
20
 
21
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8tracdap/rt/_impl/grpc/tracdap/api/internal/runtime.proto\x12\x14tracdap.api.internal\x1a\x36tracdap/rt/_impl/grpc/tracdap/metadata/object_id.proto\x1a\x30tracdap/rt/_impl/grpc/tracdap/metadata/job.proto\x1a\x33tracdap/rt/_impl/grpc/tracdap/metadata/object.proto\x1a\x1cgoogle/api/annotations.proto\"6\n\x16RuntimeListJobsRequest\x12\x12\n\x05limit\x18\x01 \x01(\rH\x00\x88\x01\x01\x42\x08\n\x06_limit\"O\n\x17RuntimeListJobsResponse\x12\x34\n\x04jobs\x18\x01 \x03(\x0b\x32&.tracdap.api.internal.RuntimeJobStatus\"f\n\x15RuntimeJobInfoRequest\x12\x34\n\x0bjobSelector\x18\x01 \x01(\x0b\x32\x1d.tracdap.metadata.TagSelectorH\x00\x12\x10\n\x06jobKey\x18\x02 \x01(\tH\x00\x42\x05\n\x03job\"\x9f\x01\n\x10RuntimeJobStatus\x12*\n\x05jobId\x18\x01 \x01(\x0b\x32\x1b.tracdap.metadata.TagHeader\x12\x33\n\nstatusCode\x18\x02 \x01(\x0e\x32\x1f.tracdap.metadata.JobStatusCode\x12\x15\n\rstatusMessage\x18\x03 \x01(\t\x12\x13\n\x0b\x65rrorDetail\x18\x04 \x01(\t\"\xa4\x02\n\x10RuntimeJobResult\x12*\n\x05jobId\x18\x01 \x01(\x0b\x32\x1b.tracdap.metadata.TagHeader\x12\x33\n\nstatusCode\x18\x02 \x01(\x0e\x32\x1f.tracdap.metadata.JobStatusCode\x12\x15\n\rstatusMessage\x18\x03 \x01(\t\x12\x44\n\x07results\x18\x04 \x03(\x0b\x32\x33.tracdap.api.internal.RuntimeJobResult.ResultsEntry\x1aR\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x31\n\x05value\x18\x02 \x01(\x0b\x32\".tracdap.metadata.ObjectDefinition:\x02\x38\x01\x32\x95\x03\n\x0eTracRuntimeApi\x12{\n\x08listJobs\x12,.tracdap.api.internal.RuntimeListJobsRequest\x1a-.tracdap.api.internal.RuntimeListJobsResponse\"\x12\x82\xd3\xe4\x93\x02\x0c\x12\n/list-jobs\x12\x81\x01\n\x0cgetJobStatus\x12+.tracdap.api.internal.RuntimeJobInfoRequest\x1a&.tracdap.api.internal.RuntimeJobStatus\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/job-status/{jobKey}\x12\x81\x01\n\x0cgetJobResult\x12+.tracdap.api.internal.RuntimeJobInfoRequest\x1a&.tracdap.api.internal.RuntimeJobResult\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/job-result/{jobKey}B\"\n\x1eorg.finos.tracdap.api.internalP\x01\x62\x06proto3')
21
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8tracdap/rt/_impl/grpc/tracdap/api/internal/runtime.proto\x12\x14tracdap.api.internal\x1a\x36tracdap/rt/_impl/grpc/tracdap/metadata/object_id.proto\x1a\x30tracdap/rt/_impl/grpc/tracdap/metadata/job.proto\x1a\x33tracdap/rt/_impl/grpc/tracdap/metadata/object.proto\x1a\x1cgoogle/api/annotations.proto\"6\n\x16RuntimeListJobsRequest\x12\x12\n\x05limit\x18\x01 \x01(\rH\x00\x88\x01\x01\x42\x08\n\x06_limit\"O\n\x17RuntimeListJobsResponse\x12\x34\n\x04jobs\x18\x01 \x03(\x0b\x32&.tracdap.api.internal.RuntimeJobStatus\"f\n\x15RuntimeJobInfoRequest\x12\x34\n\x0bjobSelector\x18\x01 \x01(\x0b\x32\x1d.tracdap.metadata.TagSelectorH\x00\x12\x10\n\x06jobKey\x18\x02 \x01(\tH\x00\x42\x05\n\x03job\"\x9f\x01\n\x10RuntimeJobStatus\x12*\n\x05jobId\x18\x01 \x01(\x0b\x32\x1b.tracdap.metadata.TagHeader\x12\x33\n\nstatusCode\x18\x02 \x01(\x0e\x32\x1f.tracdap.metadata.JobStatusCode\x12\x15\n\rstatusMessage\x18\x03 \x01(\t\x12\x13\n\x0b\x65rrorDetail\x18\x04 \x01(\t\"\xa4\x02\n\x10RuntimeJobResult\x12*\n\x05jobId\x18\x01 \x01(\x0b\x32\x1b.tracdap.metadata.TagHeader\x12\x33\n\nstatusCode\x18\x02 \x01(\x0e\x32\x1f.tracdap.metadata.JobStatusCode\x12\x15\n\rstatusMessage\x18\x03 \x01(\t\x12\x44\n\x07results\x18\x04 \x03(\x0b\x32\x33.tracdap.api.internal.RuntimeJobResult.ResultsEntry\x1aR\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x31\n\x05value\x18\x02 \x01(\x0b\x32\".tracdap.metadata.ObjectDefinition:\x02\x38\x01\x32\x95\x03\n\x0eTracRuntimeApi\x12{\n\x08listJobs\x12,.tracdap.api.internal.RuntimeListJobsRequest\x1a-.tracdap.api.internal.RuntimeListJobsResponse\"\x12\x82\xd3\xe4\x93\x02\x0c\x12\n/list-jobs\x12\x81\x01\n\x0cgetJobStatus\x12+.tracdap.api.internal.RuntimeJobInfoRequest\x1a&.tracdap.api.internal.RuntimeJobStatus\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/job-status/{jobKey}\x12\x81\x01\n\x0cgetJobResult\x12+.tracdap.api.internal.RuntimeJobInfoRequest\x1a&.tracdap.api.internal.RuntimeJobResult\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/job-result/{jobKey}B7\n\x1eorg.finos.tracdap.api.internalB\x13RuntimeServiceProtoP\x01\x62\x06proto3')
22
22
 
23
23
  _globals = globals()
24
24
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
25
25
  _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tracdap.rt._impl.grpc.tracdap.api.internal.runtime_pb2', _globals)
26
26
  if _descriptor._USE_C_DESCRIPTORS == False:
27
27
  _globals['DESCRIPTOR']._options = None
28
- _globals['DESCRIPTOR']._serialized_options = b'\n\036org.finos.tracdap.api.internalP\001'
28
+ _globals['DESCRIPTOR']._serialized_options = b'\n\036org.finos.tracdap.api.internalB\023RuntimeServiceProtoP\001'
29
29
  _globals['_RUNTIMEJOBRESULT_RESULTSENTRY']._options = None
30
30
  _globals['_RUNTIMEJOBRESULT_RESULTSENTRY']._serialized_options = b'8\001'
31
31
  _globals['_TRACRUNTIMEAPI'].methods_by_name['listJobs']._options = None
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from tracdap.rt._impl.grpc.tracdap.api.internal import runtime_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_api_dot_internal_dot_runtime__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.66.1'
8
+ GRPC_GENERATED_VERSION = '1.70.0'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # source: tracdap/rt/_impl/grpc/tracdap/metadata/common.proto
4
- # Protobuf Python Version: 4.25.3
4
+ # Protobuf Python Version: 4.25.5
5
5
  """Generated protocol buffer code."""
6
6
  from google.protobuf import descriptor as _descriptor
7
7
  from google.protobuf import descriptor_pool as _descriptor_pool