tracdap-runtime 0.6.2__py3-none-any.whl → 0.6.4__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 (71) hide show
  1. tracdap/rt/_exec/actors.py +87 -10
  2. tracdap/rt/_exec/context.py +207 -100
  3. tracdap/rt/_exec/dev_mode.py +52 -20
  4. tracdap/rt/_exec/engine.py +79 -14
  5. tracdap/rt/_exec/functions.py +14 -17
  6. tracdap/rt/_exec/runtime.py +83 -40
  7. tracdap/rt/_exec/server.py +306 -29
  8. tracdap/rt/_impl/config_parser.py +219 -49
  9. tracdap/rt/_impl/data.py +70 -5
  10. tracdap/rt/_impl/grpc/codec.py +60 -5
  11. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.py +19 -19
  12. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.pyi +11 -9
  13. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2_grpc.py +25 -25
  14. tracdap/rt/_impl/grpc/tracdap/metadata/data_pb2.py +18 -18
  15. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +28 -16
  16. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +37 -6
  17. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.py +8 -3
  18. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.pyi +13 -2
  19. tracdap/rt/_impl/guard_rails.py +21 -0
  20. tracdap/rt/_impl/models.py +25 -0
  21. tracdap/rt/_impl/static_api.py +43 -13
  22. tracdap/rt/_impl/type_system.py +17 -0
  23. tracdap/rt/_impl/validation.py +47 -4
  24. tracdap/rt/_plugins/config_local.py +49 -0
  25. tracdap/rt/_version.py +1 -1
  26. tracdap/rt/api/hook.py +6 -5
  27. tracdap/rt/api/model_api.py +50 -7
  28. tracdap/rt/api/static_api.py +81 -23
  29. tracdap/rt/config/__init__.py +4 -4
  30. tracdap/rt/config/common.py +25 -15
  31. tracdap/rt/config/job.py +2 -2
  32. tracdap/rt/config/platform.py +25 -35
  33. tracdap/rt/config/result.py +2 -2
  34. tracdap/rt/config/runtime.py +4 -2
  35. tracdap/rt/ext/config.py +34 -0
  36. tracdap/rt/ext/embed.py +1 -3
  37. tracdap/rt/ext/plugins.py +47 -6
  38. tracdap/rt/launch/cli.py +11 -4
  39. tracdap/rt/launch/launch.py +53 -12
  40. tracdap/rt/metadata/__init__.py +17 -17
  41. tracdap/rt/metadata/common.py +2 -2
  42. tracdap/rt/metadata/custom.py +3 -3
  43. tracdap/rt/metadata/data.py +12 -12
  44. tracdap/rt/metadata/file.py +6 -6
  45. tracdap/rt/metadata/flow.py +6 -6
  46. tracdap/rt/metadata/job.py +8 -8
  47. tracdap/rt/metadata/model.py +21 -11
  48. tracdap/rt/metadata/object.py +3 -0
  49. tracdap/rt/metadata/object_id.py +8 -8
  50. tracdap/rt/metadata/search.py +5 -5
  51. tracdap/rt/metadata/stoarge.py +6 -6
  52. tracdap/rt/metadata/tag.py +1 -1
  53. tracdap/rt/metadata/tag_update.py +1 -1
  54. tracdap/rt/metadata/type.py +4 -4
  55. {tracdap_runtime-0.6.2.dist-info → tracdap_runtime-0.6.4.dist-info}/METADATA +4 -4
  56. tracdap_runtime-0.6.4.dist-info/RECORD +112 -0
  57. {tracdap_runtime-0.6.2.dist-info → tracdap_runtime-0.6.4.dist-info}/WHEEL +1 -1
  58. tracdap/rt/_impl/grpc/tracdap/config/common_pb2.py +0 -55
  59. tracdap/rt/_impl/grpc/tracdap/config/common_pb2.pyi +0 -103
  60. tracdap/rt/_impl/grpc/tracdap/config/job_pb2.py +0 -42
  61. tracdap/rt/_impl/grpc/tracdap/config/job_pb2.pyi +0 -44
  62. tracdap/rt/_impl/grpc/tracdap/config/platform_pb2.py +0 -71
  63. tracdap/rt/_impl/grpc/tracdap/config/platform_pb2.pyi +0 -197
  64. tracdap/rt/_impl/grpc/tracdap/config/result_pb2.py +0 -37
  65. tracdap/rt/_impl/grpc/tracdap/config/result_pb2.pyi +0 -35
  66. tracdap/rt/_impl/grpc/tracdap/config/runtime_pb2.py +0 -42
  67. tracdap/rt/_impl/grpc/tracdap/config/runtime_pb2.pyi +0 -46
  68. tracdap/rt/ext/_guard.py +0 -37
  69. tracdap_runtime-0.6.2.dist-info/RECORD +0 -121
  70. {tracdap_runtime-0.6.2.dist-info → tracdap_runtime-0.6.4.dist-info}/LICENSE +0 -0
  71. {tracdap_runtime-0.6.2.dist-info → tracdap_runtime-0.6.4.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,7 @@ _sym_db = _symbol_database.Default()
13
13
 
14
14
 
15
15
  from tracdap.rt._impl.grpc.tracdap.metadata import object_id_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_metadata_dot_object__id__pb2
16
+ from tracdap.rt._impl.grpc.tracdap.metadata import type_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_metadata_dot_type__pb2
16
17
  from tracdap.rt._impl.grpc.tracdap.metadata import data_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_metadata_dot_data__pb2
17
18
  from tracdap.rt._impl.grpc.tracdap.metadata import model_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_metadata_dot_model__pb2
18
19
  from tracdap.rt._impl.grpc.tracdap.metadata import flow_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_metadata_dot_flow__pb2
@@ -22,7 +23,7 @@ from tracdap.rt._impl.grpc.tracdap.metadata import custom_pb2 as tracdap_dot_rt_
22
23
  from tracdap.rt._impl.grpc.tracdap.metadata import stoarge_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_metadata_dot_stoarge__pb2
23
24
 
24
25
 
25
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n3tracdap/rt/_impl/grpc/tracdap/metadata/object.proto\x12\x10tracdap.metadata\x1a\x36tracdap/rt/_impl/grpc/tracdap/metadata/object_id.proto\x1a\x31tracdap/rt/_impl/grpc/tracdap/metadata/data.proto\x1a\x32tracdap/rt/_impl/grpc/tracdap/metadata/model.proto\x1a\x31tracdap/rt/_impl/grpc/tracdap/metadata/flow.proto\x1a\x30tracdap/rt/_impl/grpc/tracdap/metadata/job.proto\x1a\x31tracdap/rt/_impl/grpc/tracdap/metadata/file.proto\x1a\x33tracdap/rt/_impl/grpc/tracdap/metadata/custom.proto\x1a\x34tracdap/rt/_impl/grpc/tracdap/metadata/stoarge.proto\"\xf0\x03\n\x10ObjectDefinition\x12\x30\n\nobjectType\x18\x01 \x01(\x0e\x32\x1c.tracdap.metadata.ObjectType\x12\x30\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32 .tracdap.metadata.DataDefinitionH\x00\x12\x32\n\x05model\x18\x03 \x01(\x0b\x32!.tracdap.metadata.ModelDefinitionH\x00\x12\x30\n\x04\x66low\x18\x04 \x01(\x0b\x32 .tracdap.metadata.FlowDefinitionH\x00\x12.\n\x03job\x18\x05 \x01(\x0b\x32\x1f.tracdap.metadata.JobDefinitionH\x00\x12\x30\n\x04\x66ile\x18\x06 \x01(\x0b\x32 .tracdap.metadata.FileDefinitionH\x00\x12\x34\n\x06\x63ustom\x18\x07 \x01(\x0b\x32\".tracdap.metadata.CustomDefinitionH\x00\x12\x36\n\x07storage\x18\x08 \x01(\x0b\x32#.tracdap.metadata.StorageDefinitionH\x00\x12\x34\n\x06schema\x18\t \x01(\x0b\x32\".tracdap.metadata.SchemaDefinitionH\x00\x42\x0c\n\ndefinitionB2\n\x1aorg.finos.tracdap.metadataB\x12ObjectProtoWrapperP\x01\x62\x06proto3')
26
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n3tracdap/rt/_impl/grpc/tracdap/metadata/object.proto\x12\x10tracdap.metadata\x1a\x36tracdap/rt/_impl/grpc/tracdap/metadata/object_id.proto\x1a\x31tracdap/rt/_impl/grpc/tracdap/metadata/type.proto\x1a\x31tracdap/rt/_impl/grpc/tracdap/metadata/data.proto\x1a\x32tracdap/rt/_impl/grpc/tracdap/metadata/model.proto\x1a\x31tracdap/rt/_impl/grpc/tracdap/metadata/flow.proto\x1a\x30tracdap/rt/_impl/grpc/tracdap/metadata/job.proto\x1a\x31tracdap/rt/_impl/grpc/tracdap/metadata/file.proto\x1a\x33tracdap/rt/_impl/grpc/tracdap/metadata/custom.proto\x1a\x34tracdap/rt/_impl/grpc/tracdap/metadata/stoarge.proto\"\x87\x05\n\x10ObjectDefinition\x12\x30\n\nobjectType\x18\x01 \x01(\x0e\x32\x1c.tracdap.metadata.ObjectType\x12\x30\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32 .tracdap.metadata.DataDefinitionH\x00\x12\x32\n\x05model\x18\x03 \x01(\x0b\x32!.tracdap.metadata.ModelDefinitionH\x00\x12\x30\n\x04\x66low\x18\x04 \x01(\x0b\x32 .tracdap.metadata.FlowDefinitionH\x00\x12.\n\x03job\x18\x05 \x01(\x0b\x32\x1f.tracdap.metadata.JobDefinitionH\x00\x12\x30\n\x04\x66ile\x18\x06 \x01(\x0b\x32 .tracdap.metadata.FileDefinitionH\x00\x12\x34\n\x06\x63ustom\x18\x07 \x01(\x0b\x32\".tracdap.metadata.CustomDefinitionH\x00\x12\x36\n\x07storage\x18\x08 \x01(\x0b\x32#.tracdap.metadata.StorageDefinitionH\x00\x12\x34\n\x06schema\x18\t \x01(\x0b\x32\".tracdap.metadata.SchemaDefinitionH\x00\x12H\n\x0bobjectProps\x18\x64 \x03(\x0b\x32\x33.tracdap.metadata.ObjectDefinition.ObjectPropsEntry\x1aK\n\x10ObjectPropsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.tracdap.metadata.Value:\x02\x38\x01\x42\x0c\n\ndefinitionB2\n\x1aorg.finos.tracdap.metadataB\x12ObjectProtoWrapperP\x01\x62\x06proto3')
26
27
 
27
28
  _globals = globals()
28
29
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -30,6 +31,10 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tracdap.rt._impl.grpc.tracd
30
31
  if _descriptor._USE_C_DESCRIPTORS == False:
31
32
  _globals['DESCRIPTOR']._options = None
32
33
  _globals['DESCRIPTOR']._serialized_options = b'\n\032org.finos.tracdap.metadataB\022ObjectProtoWrapperP\001'
33
- _globals['_OBJECTDEFINITION']._serialized_start=492
34
- _globals['_OBJECTDEFINITION']._serialized_end=988
34
+ _globals['_OBJECTDEFINITION_OBJECTPROPSENTRY']._options = None
35
+ _globals['_OBJECTDEFINITION_OBJECTPROPSENTRY']._serialized_options = b'8\001'
36
+ _globals['_OBJECTDEFINITION']._serialized_start=543
37
+ _globals['_OBJECTDEFINITION']._serialized_end=1190
38
+ _globals['_OBJECTDEFINITION_OBJECTPROPSENTRY']._serialized_start=1101
39
+ _globals['_OBJECTDEFINITION_OBJECTPROPSENTRY']._serialized_end=1176
35
40
  # @@protoc_insertion_point(module_scope)
@@ -1,4 +1,5 @@
1
1
  from tracdap.rt._impl.grpc.tracdap.metadata import object_id_pb2 as _object_id_pb2
2
+ from tracdap.rt._impl.grpc.tracdap.metadata import type_pb2 as _type_pb2
2
3
  from tracdap.rt._impl.grpc.tracdap.metadata import data_pb2 as _data_pb2
3
4
  from tracdap.rt._impl.grpc.tracdap.metadata import model_pb2 as _model_pb2
4
5
  from tracdap.rt._impl.grpc.tracdap.metadata import flow_pb2 as _flow_pb2
@@ -6,6 +7,7 @@ from tracdap.rt._impl.grpc.tracdap.metadata import job_pb2 as _job_pb2
6
7
  from tracdap.rt._impl.grpc.tracdap.metadata import file_pb2 as _file_pb2
7
8
  from tracdap.rt._impl.grpc.tracdap.metadata import custom_pb2 as _custom_pb2
8
9
  from tracdap.rt._impl.grpc.tracdap.metadata import stoarge_pb2 as _stoarge_pb2
10
+ from google.protobuf.internal import containers as _containers
9
11
  from google.protobuf import descriptor as _descriptor
10
12
  from google.protobuf import message as _message
11
13
  from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
@@ -13,7 +15,14 @@ from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Opti
13
15
  DESCRIPTOR: _descriptor.FileDescriptor
14
16
 
15
17
  class ObjectDefinition(_message.Message):
16
- __slots__ = ("objectType", "data", "model", "flow", "job", "file", "custom", "storage", "schema")
18
+ __slots__ = ("objectType", "data", "model", "flow", "job", "file", "custom", "storage", "schema", "objectProps")
19
+ class ObjectPropsEntry(_message.Message):
20
+ __slots__ = ("key", "value")
21
+ KEY_FIELD_NUMBER: _ClassVar[int]
22
+ VALUE_FIELD_NUMBER: _ClassVar[int]
23
+ key: str
24
+ value: _type_pb2.Value
25
+ def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[_type_pb2.Value, _Mapping]] = ...) -> None: ...
17
26
  OBJECTTYPE_FIELD_NUMBER: _ClassVar[int]
18
27
  DATA_FIELD_NUMBER: _ClassVar[int]
19
28
  MODEL_FIELD_NUMBER: _ClassVar[int]
@@ -23,6 +32,7 @@ class ObjectDefinition(_message.Message):
23
32
  CUSTOM_FIELD_NUMBER: _ClassVar[int]
24
33
  STORAGE_FIELD_NUMBER: _ClassVar[int]
25
34
  SCHEMA_FIELD_NUMBER: _ClassVar[int]
35
+ OBJECTPROPS_FIELD_NUMBER: _ClassVar[int]
26
36
  objectType: _object_id_pb2.ObjectType
27
37
  data: _data_pb2.DataDefinition
28
38
  model: _model_pb2.ModelDefinition
@@ -32,4 +42,5 @@ class ObjectDefinition(_message.Message):
32
42
  custom: _custom_pb2.CustomDefinition
33
43
  storage: _stoarge_pb2.StorageDefinition
34
44
  schema: _data_pb2.SchemaDefinition
35
- def __init__(self, objectType: _Optional[_Union[_object_id_pb2.ObjectType, str]] = ..., data: _Optional[_Union[_data_pb2.DataDefinition, _Mapping]] = ..., model: _Optional[_Union[_model_pb2.ModelDefinition, _Mapping]] = ..., flow: _Optional[_Union[_flow_pb2.FlowDefinition, _Mapping]] = ..., job: _Optional[_Union[_job_pb2.JobDefinition, _Mapping]] = ..., file: _Optional[_Union[_file_pb2.FileDefinition, _Mapping]] = ..., custom: _Optional[_Union[_custom_pb2.CustomDefinition, _Mapping]] = ..., storage: _Optional[_Union[_stoarge_pb2.StorageDefinition, _Mapping]] = ..., schema: _Optional[_Union[_data_pb2.SchemaDefinition, _Mapping]] = ...) -> None: ...
45
+ objectProps: _containers.MessageMap[str, _type_pb2.Value]
46
+ def __init__(self, objectType: _Optional[_Union[_object_id_pb2.ObjectType, str]] = ..., data: _Optional[_Union[_data_pb2.DataDefinition, _Mapping]] = ..., model: _Optional[_Union[_model_pb2.ModelDefinition, _Mapping]] = ..., flow: _Optional[_Union[_flow_pb2.FlowDefinition, _Mapping]] = ..., job: _Optional[_Union[_job_pb2.JobDefinition, _Mapping]] = ..., file: _Optional[_Union[_file_pb2.FileDefinition, _Mapping]] = ..., custom: _Optional[_Union[_custom_pb2.CustomDefinition, _Mapping]] = ..., storage: _Optional[_Union[_stoarge_pb2.StorageDefinition, _Mapping]] = ..., schema: _Optional[_Union[_data_pb2.SchemaDefinition, _Mapping]] = ..., objectProps: _Optional[_Mapping[str, _type_pb2.Value]] = ...) -> None: ...
@@ -46,6 +46,27 @@ def _get_package_path(module_name):
46
46
  return module_path.parents[depth]
47
47
 
48
48
 
49
+ def run_model_guard(operation: str = None):
50
+
51
+ # A simple guard method to block model code from accessing parts of the TRAC runtime framework
52
+ # To blocks calls to the Python stdlib or 3rd party libs, use PythonGuardRails instead
53
+
54
+ stack = inspect.stack()
55
+ frame = stack[-1]
56
+
57
+ if operation is None:
58
+ operation = f"Calling {frame.function}()"
59
+
60
+ for frame_index in range(len(stack) - 2, 0, -1):
61
+
62
+ parent_frame = frame
63
+ frame = stack[frame_index]
64
+
65
+ if frame.function == "run_model" and parent_frame.function == "_execute":
66
+ err = f"{operation} is not allowed inside run_model()"
67
+ raise ex.ERuntimeValidation(err)
68
+
69
+
49
70
  class PythonGuardRails:
50
71
 
51
72
  DANGEROUS_BUILTIN_FUNCTIONS = ["exec", "eval", "compile", "open", "input", "memoryview"]
@@ -215,12 +215,15 @@ class ModelLoader:
215
215
 
216
216
  for name, param in model_def.parameters.items():
217
217
  self.__log.info(f"Parameter [{name}] - {param.paramType.basicType.name}")
218
+ param.paramProps = self._encoded_props(param.paramProps, "parameter", name)
218
219
 
219
220
  for name, schema in model_def.inputs.items():
220
221
  self.__log.info(f"Input [{name}] - {schema.schema.schemaType.name}")
222
+ schema.inputProps = self._encoded_props(schema.inputProps, "input", name)
221
223
 
222
224
  for name, schema in model_def.outputs.items():
223
225
  self.__log.info(f"Output [{name}] - {schema.schema.schemaType.name}")
226
+ schema.outputProps = self._encoded_props(schema.outputProps, "input", name)
224
227
 
225
228
  return model_def
226
229
 
@@ -231,3 +234,25 @@ class ModelLoader:
231
234
 
232
235
  self.__log.error(msg, exc_info=True)
233
236
  raise _ex.EModelValidation(msg) from e
237
+
238
+ @staticmethod
239
+ def _encoded_props(
240
+ raw_props: tp.Dict[str, tp.Any],
241
+ item_type: str, item_name: str) \
242
+ -> tp.Dict[str, _meta.Value]:
243
+
244
+ if raw_props is None:
245
+ return dict()
246
+
247
+ encoded_props = dict()
248
+
249
+ for key, raw_value in raw_props.items():
250
+
251
+ if raw_value is None:
252
+ raise _ex.EModelValidation(f"Invalid null property [{key}] for {item_type} [{item_name}]")
253
+ elif isinstance(raw_value, _meta.Value):
254
+ encoded_props[key] = raw_value
255
+ else:
256
+ encoded_props[key] = _types.MetadataCodec.encode_value(raw_value)
257
+
258
+ return encoded_props
@@ -71,10 +71,14 @@ class StaticApiImpl(_StaticApiHook):
71
71
 
72
72
  def define_parameter(
73
73
  self, param_name: str, param_type: _tp.Union[_meta.TypeDescriptor, _meta.BasicType],
74
- label: str, default_value: _tp.Optional[_tp.Any] = None) \
74
+ label: str, default_value: _tp.Optional[_tp.Any] = None,
75
+ *, param_props: _tp.Optional[_tp.Dict[str, _tp.Any]] = None) \
75
76
  -> _Named[_meta.ModelParameter]:
76
77
 
77
- _val.validate_signature(self.define_parameter, param_name, param_type, label, default_value)
78
+ _val.validate_signature(
79
+ self.define_parameter,
80
+ param_name, param_type, label, default_value,
81
+ param_props=param_props)
78
82
 
79
83
  if isinstance(param_type, _meta.TypeDescriptor):
80
84
  param_type_descriptor = param_type
@@ -88,7 +92,9 @@ class StaticApiImpl(_StaticApiHook):
88
92
  msg = f"Default value for parameter [{param_name}] does not match the declared type"
89
93
  raise _ex.EModelValidation(msg) from e
90
94
 
91
- return _Named(param_name, _meta.ModelParameter(param_type_descriptor, label, default_value))
95
+ return _Named(param_name, _meta.ModelParameter(
96
+ param_type_descriptor, label, default_value,
97
+ paramProps=param_props))
92
98
 
93
99
  def define_parameters(
94
100
  self, *params: _tp.Union[_Named[_meta.ModelParameter], _tp.List[_Named[_meta.ModelParameter]]]) \
@@ -148,25 +154,49 @@ class StaticApiImpl(_StaticApiHook):
148
154
 
149
155
  def define_input_table(
150
156
  self, *fields: _tp.Union[_meta.FieldSchema, _tp.List[_meta.FieldSchema]],
151
- label: _tp.Optional[str] = None,
152
- optional: bool = False) \
157
+ label: _tp.Optional[str] = None, optional: bool = False, dynamic: bool = False,
158
+ input_props: _tp.Optional[_tp.Dict[str, _tp.Any]] = None) \
153
159
  -> _meta.ModelInputSchema:
154
160
 
155
- _val.validate_signature(self.define_input_table, *fields, label=label, optional=optional)
161
+ _val.validate_signature(
162
+ self.define_input_table, *fields,
163
+ label=label, optional=optional, dynamic=dynamic,
164
+ input_props=input_props)
165
+
166
+ # Do not define details for dynamic schemas
167
+
168
+ if dynamic:
169
+ schema_def = _meta.SchemaDefinition(_meta.SchemaType.TABLE)
170
+ else:
171
+ schema_def = self.define_schema(*fields, schema_type=_meta.SchemaType.TABLE)
156
172
 
157
- schema_def = self.define_schema(*fields, schema_type=_meta.SchemaType.TABLE)
158
- return _meta.ModelInputSchema(schema=schema_def, label=label, optional=optional)
173
+ return _meta.ModelInputSchema(
174
+ schema=schema_def, label=label,
175
+ optional=optional, dynamic=dynamic,
176
+ inputProps=input_props)
159
177
 
160
178
  def define_output_table(
161
179
  self, *fields: _tp.Union[_meta.FieldSchema, _tp.List[_meta.FieldSchema]],
162
- label: _tp.Optional[str] = None,
163
- optional: bool = False) \
180
+ label: _tp.Optional[str] = None, optional: bool = False, dynamic: bool = False,
181
+ output_props: _tp.Optional[_tp.Dict[str, _tp.Any]] = None) \
164
182
  -> _meta.ModelOutputSchema:
165
183
 
166
- _val.validate_signature(self.define_output_table, *fields, label=label, optional=optional)
184
+ _val.validate_signature(
185
+ self.define_output_table, *fields,
186
+ label=label, optional=optional, dynamic=dynamic,
187
+ output_props=output_props)
188
+
189
+ # Do not define details for dynamic schemas
190
+
191
+ if dynamic:
192
+ schema_def = _meta.SchemaDefinition(_meta.SchemaType.TABLE)
193
+ else:
194
+ schema_def = self.define_schema(*fields, schema_type=_meta.SchemaType.TABLE)
167
195
 
168
- schema_def = self.define_schema(*fields, schema_type=_meta.SchemaType.TABLE)
169
- return _meta.ModelOutputSchema(schema=schema_def, label=label, optional=optional)
196
+ return _meta.ModelOutputSchema(
197
+ schema=schema_def, label=label,
198
+ optional=optional, dynamic=dynamic,
199
+ outputProps=output_props)
170
200
 
171
201
  @staticmethod
172
202
  def _build_named_dict(
@@ -171,6 +171,23 @@ class MetadataCodec:
171
171
  type_desc = _meta.TypeDescriptor(_meta.BasicType.DATE)
172
172
  return _meta.Value(type_desc, dateValue=_meta.DateValue(value.isoformat()))
173
173
 
174
+ if isinstance(value, list):
175
+
176
+ if len(value) == 0:
177
+ raise _ex.ETracInternal("Cannot encode an empty list")
178
+
179
+ array_raw_type = type(value[0])
180
+ array_trac_type = TypeMapping.python_to_trac(array_raw_type)
181
+
182
+ if any(map(lambda x: type(x) != array_raw_type, value)):
183
+ raise _ex.ETracInternal("Cannot encode a list with values of different types")
184
+
185
+ encoded_items = list(map(lambda x: cls.convert_value(x, array_trac_type), value))
186
+
187
+ return _meta.Value(
188
+ _meta.TypeDescriptor(_meta.BasicType.ARRAY, arrayType=array_trac_type),
189
+ arrayValue=_meta.ArrayValue(encoded_items))
190
+
174
191
  raise _ex.ETracInternal(f"Value type [{type(value)}] is not supported yet")
175
192
 
176
193
  @classmethod
@@ -38,7 +38,7 @@ def check_type(expected_type: tp.Type, value: tp.Any) -> bool:
38
38
 
39
39
 
40
40
  def quick_validate_model_def(model_def: meta.ModelDefinition):
41
- _StaticValidator.quick_validate_model_def(model_def)
41
+ StaticValidator.quick_validate_model_def(model_def)
42
42
 
43
43
 
44
44
  class _TypeValidator:
@@ -233,7 +233,7 @@ class _TypeValidator:
233
233
  return type_var.__name__
234
234
 
235
235
 
236
- class _StaticValidator:
236
+ class StaticValidator:
237
237
 
238
238
  __identifier_pattern = re.compile("\\A[a-zA-Z_]\\w*\\Z", re.ASCII)
239
239
  __reserved_identifier_pattern = re.compile("\\A(_|trac_)", re.ASCII)
@@ -301,6 +301,28 @@ class _StaticValidator:
301
301
  cls._check_inputs_or_outputs(model_def.inputs)
302
302
  cls._check_inputs_or_outputs(model_def.outputs)
303
303
 
304
+ @classmethod
305
+ def quick_validate_schema(cls, schema: meta.SchemaDefinition):
306
+
307
+ if schema.schemaType != meta.SchemaType.TABLE:
308
+ cls._fail(f"Unsupported schema type [{schema.schemaType}]")
309
+
310
+ if schema.partType != meta.PartType.PART_ROOT:
311
+ cls._fail(f"Unsupported partition type [{schema.partType}]")
312
+
313
+ if schema.table is None or schema.table.fields is None or len(schema.table.fields) == 0:
314
+ cls._fail(f"Table schema does not define any fields")
315
+
316
+ fields = schema.table.fields
317
+ field_names = list(map(lambda f: f.fieldName, fields))
318
+ property_type = f"field"
319
+
320
+ cls._valid_identifiers(field_names, property_type)
321
+ cls._case_insensitive_duplicates(field_names, property_type)
322
+
323
+ for field in fields:
324
+ cls._check_single_field(field, property_type)
325
+
304
326
  @classmethod
305
327
  def _check_label(cls, label, param_name):
306
328
  if label is not None:
@@ -320,6 +342,9 @@ class _StaticValidator:
320
342
  else:
321
343
  cls._check_label(param.label, param_name)
322
344
 
345
+ if param.paramProps is not None:
346
+ cls._valid_identifiers(param.paramProps.keys(), "entry in param props")
347
+
323
348
  @classmethod
324
349
  def _check_inputs_or_outputs(cls, inputs_or_outputs):
325
350
 
@@ -327,10 +352,20 @@ class _StaticValidator:
327
352
 
328
353
  cls._log.info(f"Checking {input_name}")
329
354
 
355
+ if input_schema.dynamic:
356
+ if input_schema.schema and input_schema.schema.table:
357
+ error = "Dynamic schemas must have schema.table = None"
358
+ cls._fail(f"Invalid schema for [{input_name}]: {error}")
359
+ else:
360
+ continue
361
+
330
362
  fields = input_schema.schema.table.fields
331
363
  field_names = list(map(lambda f: f.fieldName, fields))
332
364
  property_type = f"field in [{input_name}]"
333
365
 
366
+ if len(fields) == 0:
367
+ cls._fail(f"Invalid schema for [{input_name}]: No fields defined")
368
+
334
369
  cls._valid_identifiers(field_names, property_type)
335
370
  cls._case_insensitive_duplicates(field_names, property_type)
336
371
 
@@ -340,6 +375,13 @@ class _StaticValidator:
340
375
  label = input_schema.label
341
376
  cls._check_label(label, input_name)
342
377
 
378
+ if isinstance(input_schema, meta.ModelInputSchema):
379
+ if input_schema.inputProps is not None:
380
+ cls._valid_identifiers(input_schema.inputProps.keys(), "entry in input props")
381
+ else:
382
+ if input_schema.outputProps is not None:
383
+ cls._valid_identifiers(input_schema.outputProps.keys(), "entry in output props")
384
+
343
385
  @classmethod
344
386
  def _check_single_field(cls, field: meta.FieldSchema, property_type):
345
387
 
@@ -365,8 +407,9 @@ class _StaticValidator:
365
407
  if field.categorical and field.fieldType != meta.BasicType.STRING:
366
408
  cls._fail(f"Invalid {property_type}: [{field.fieldName}] fieldType {field.fieldType} used as categorical")
367
409
 
368
- if field.businessKey and not field.notNull:
369
- cls._fail(f"Invalid {property_type}: [{field.fieldName}] is a business key but not_null = False")
410
+ # Do not require notNull = True for business keys here
411
+ # Instead setting businessKey = True will cause notNull = True to be set during normalization
412
+ # This agrees with the semantics in platform API and CSV schema loader
370
413
 
371
414
  @classmethod
372
415
  def _valid_identifiers(cls, keys, property_type):
@@ -0,0 +1,49 @@
1
+ # Copyright 2024 Accenture Global Solutions Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import pathlib as pathlib
16
+ import typing as tp
17
+
18
+ import tracdap.rt.ext.plugins as plugins
19
+ import tracdap.rt.exceptions as ex
20
+
21
+ from tracdap.rt.ext.config import *
22
+
23
+
24
+ class LocalConfigLoader(IConfigLoader):
25
+
26
+ # Properties dict will be empty for config plugins
27
+ def __init__(self, properties: tp.Dict[str, str]): # noqa
28
+ pass
29
+
30
+ def has_config_file(self, config_url: str) -> bool:
31
+ if config_url.startswith("file:"):
32
+ config_url = config_url[5:]
33
+ config_path = pathlib.Path(config_url).resolve()
34
+ return config_path.exists() and config_path.is_file()
35
+
36
+ def load_config_file(self, config_url: str) -> bytes:
37
+ if config_url.startswith("file:"):
38
+ config_url = config_url[5:]
39
+ config_path = pathlib.Path(config_url).resolve()
40
+ return config_path.read_bytes()
41
+
42
+ def has_config_dict(self, config_url: str) -> bool:
43
+ return False
44
+
45
+ def load_config_dict(self, config_url: str) -> dict:
46
+ raise ex.ETracInternal("Local config loader does not support loading objects")
47
+
48
+
49
+ plugins.PluginManager.register_plugin(IConfigLoader, LocalConfigLoader, ["LOCAL", "file"])
tracdap/rt/_version.py CHANGED
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "0.6.2"
15
+ __version__ = "0.6.4"
tracdap/rt/api/hook.py CHANGED
@@ -80,7 +80,8 @@ class _StaticApiHook:
80
80
  @_abc.abstractmethod
81
81
  def define_parameter(
82
82
  self, param_name: str, param_type: _tp.Union[_meta.TypeDescriptor, _meta.BasicType],
83
- label: str, default_value: _tp.Optional[_tp.Any] = None) \
83
+ label: str, default_value: _tp.Optional[_tp.Any] = None,
84
+ *, param_props: _tp.Optional[_tp.Dict[str, _tp.Any]] = None) \
84
85
  -> _Named[_meta.ModelParameter]:
85
86
 
86
87
  pass
@@ -120,8 +121,8 @@ class _StaticApiHook:
120
121
  @_abc.abstractmethod
121
122
  def define_input_table(
122
123
  self, *fields: _tp.Union[_meta.FieldSchema, _tp.List[_meta.FieldSchema]],
123
- label: _tp.Optional[str] = None,
124
- optional: bool = False) \
124
+ label: _tp.Optional[str] = None, optional: bool = False, dynamic: bool = False,
125
+ input_props: _tp.Optional[_tp.Dict[str, _tp.Any]] = None) \
125
126
  -> _meta.ModelInputSchema:
126
127
 
127
128
  pass
@@ -129,8 +130,8 @@ class _StaticApiHook:
129
130
  @_abc.abstractmethod
130
131
  def define_output_table(
131
132
  self, *fields: _tp.Union[_meta.FieldSchema, _tp.List[_meta.FieldSchema]],
132
- label: _tp.Optional[str] = None,
133
- optional: bool = False) \
133
+ label: _tp.Optional[str] = None, optional: bool = False, dynamic: bool = False,
134
+ output_props: _tp.Optional[_tp.Dict[str, _tp.Any]] = None) \
134
135
  -> _meta.ModelOutputSchema:
135
136
 
136
137
  pass
@@ -100,13 +100,26 @@ class TracContext:
100
100
  """
101
101
  Get the schema of a model input or output
102
102
 
103
- The schema of an input or output can be retrieved and examined at runtime using this method.
104
- Inputs must be defined in :py:meth:`TracModel.define_inputs`
105
- and outputs in :py:meth:`TracModel.define_outputs`.
106
- Input and output names are case-sensitive.
107
-
108
- In the current version of the runtime all model inputs and outputs are defined statically,
109
- :py:meth:`get_schema` will return the schema as it was defined.
103
+ Use this method to get the :py:class:`SchemaDefinition <tracdap.rt.metadata.SchemaDefinition>`
104
+ for any input or output of the current model.
105
+ For datasets with static schemas, this will be the same schema that was defined in the
106
+ :py:class:`TracModel <tracdap.rt.api.TracModel>` methods
107
+ :py:meth:`define_inputs() <tracdap.rt.api.TracModel.define_inputs>` and
108
+ :py:meth:`define_outputs() <tracdap.rt.api.TracModel.define_outputs>`.
109
+
110
+ For inputs with dynamic schemas, the schema of the provided input dataset will be returned.
111
+ For outputs with dynamic schemas the schema must be set by calling
112
+ :py:meth:`put_schema() <tracdap.rt.api.TracContext.put_schema>`, after which this method
113
+ will return that schema. Calling :py:meth:`get_schema() <tracdap.rt.api.TracContext.get_schema>`
114
+ for a dynamic output before the schema is set will result in a runtime validation error.
115
+
116
+ For optional inputs, use :py:meth:`has_dataset() <tracdap.rt.api.TracContext.has_dataset>`
117
+ to check whether the input was provided. Calling :py:meth:`get_schema() <tracdap.rt.api.TracContext.get_schema>`
118
+ for an optional input that was not provided will always result in a validation error,
119
+ regardless of whether the input using a static or dynamic schema. For optional outputs
120
+ :py:meth:`get_schema() <tracdap.rt.api.TracContext.get_schema>` can be called, with the
121
+ normal proviso that dynamic schemas must first be set by calling
122
+ :py:meth:`put_schema() <tracdap.rt.api.TracContext.put_schema>`.
110
123
 
111
124
  Attempting to retrieve the schema for a dataset that is not defined as a model input or output
112
125
  will result in a runtime validation error, even if that dataset exists in the job config and
@@ -152,6 +165,36 @@ class TracContext:
152
165
  """
153
166
  pass
154
167
 
168
+ @_abc.abstractmethod
169
+ def put_schema(self, dataset_name: str, schema: SchemaDefinition):
170
+
171
+ """
172
+ Set the schema of a dynamic model output
173
+
174
+ For outputs marked as dynamic, a :py:class:`SchemaDefinition <tracdap.rt.metadata.SchemaDefinition>`
175
+ must be supplied before attempting to save the data. TRAC API functions are available to help with
176
+ building schemas, such as :py:func:`trac.F() <tracdap.rt.api.F>` to define fields or
177
+ :py:func:`load_schema() <tracdap.rt.api.load_schema>` to load predefined schemas.
178
+ Once a schema has been set, it can be retrieved by calling
179
+ :py:meth:`get_schema() <tracdap.rt.api.TracContext.get_schema>` as normal.
180
+ If :py:meth:`put_schema() <tracdap.rt.api.TracContext.put_schema>` is called for an optional
181
+ output the model must also supply data for that output, otherwise TRAC will report a runtime
182
+ validation error after the model completes.
183
+
184
+ Each schema can only be set once and the schema will be validated using the normal
185
+ validation rules. If validation fails this method will raise
186
+ :py:class:`ERuntimeValidation <tracdap.rt.exceptions.ERuntimeValidation>`.
187
+ Attempting to set the schema for a dataset that is not defined as a dynamic model output
188
+ for the current model will result in a runtime validation error.
189
+
190
+ :param dataset_name: The name of the output to set the schema for
191
+ :param schema: A TRAC schema definition to use for the named output
192
+ :type schema: :py:class:`SchemaDefinition <tracdap.rt.metadata.SchemaDefinition>`
193
+ :raises: :py:class:`ERuntimeValidation <tracdap.rt.exceptions.ERuntimeValidation>`
194
+ """
195
+
196
+ pass
197
+
155
198
  @_abc.abstractmethod
156
199
  def put_pandas_table(self, dataset_name: str, dataset: pandas.DataFrame):
157
200