tracdap-runtime 0.6.4__py3-none-any.whl → 0.6.6__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 (41) hide show
  1. tracdap/rt/_exec/context.py +556 -36
  2. tracdap/rt/_exec/dev_mode.py +320 -198
  3. tracdap/rt/_exec/engine.py +331 -62
  4. tracdap/rt/_exec/functions.py +151 -22
  5. tracdap/rt/_exec/graph.py +47 -13
  6. tracdap/rt/_exec/graph_builder.py +383 -175
  7. tracdap/rt/_exec/runtime.py +7 -5
  8. tracdap/rt/_impl/config_parser.py +11 -4
  9. tracdap/rt/_impl/data.py +329 -152
  10. tracdap/rt/_impl/ext/__init__.py +13 -0
  11. tracdap/rt/_impl/ext/sql.py +116 -0
  12. tracdap/rt/_impl/ext/storage.py +57 -0
  13. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +82 -30
  14. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +155 -2
  15. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +12 -10
  16. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -2
  17. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +29 -0
  18. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.pyi +16 -0
  19. tracdap/rt/_impl/models.py +8 -0
  20. tracdap/rt/_impl/static_api.py +29 -0
  21. tracdap/rt/_impl/storage.py +39 -27
  22. tracdap/rt/_impl/util.py +10 -0
  23. tracdap/rt/_impl/validation.py +140 -18
  24. tracdap/rt/_plugins/repo_git.py +1 -1
  25. tracdap/rt/_plugins/storage_sql.py +417 -0
  26. tracdap/rt/_plugins/storage_sql_dialects.py +117 -0
  27. tracdap/rt/_version.py +1 -1
  28. tracdap/rt/api/experimental.py +267 -0
  29. tracdap/rt/api/hook.py +14 -0
  30. tracdap/rt/api/model_api.py +48 -6
  31. tracdap/rt/config/__init__.py +2 -2
  32. tracdap/rt/config/common.py +6 -0
  33. tracdap/rt/metadata/__init__.py +29 -20
  34. tracdap/rt/metadata/job.py +99 -0
  35. tracdap/rt/metadata/model.py +18 -0
  36. tracdap/rt/metadata/resource.py +24 -0
  37. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/METADATA +5 -1
  38. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/RECORD +41 -32
  39. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/WHEEL +1 -1
  40. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/LICENSE +0 -0
  41. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,29 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: tracdap/rt/_impl/grpc/tracdap/metadata/resource.proto
4
+ # Protobuf Python Version: 4.25.3
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
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 object_pb2 as tracdap_dot_rt_dot___impl_dot_grpc_dot_tracdap_dot_metadata_dot_object__pb2
17
+
18
+
19
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5tracdap/rt/_impl/grpc/tracdap/metadata/resource.proto\x12\x10tracdap.metadata\x1a\x36tracdap/rt/_impl/grpc/tracdap/metadata/object_id.proto\x1a\x33tracdap/rt/_impl/grpc/tracdap/metadata/object.proto*m\n\x0cResourceType\x12\x19\n\x15RESOURCE_TYPE_NOT_SET\x10\x00\x12\x14\n\x10MODEL_REPOSITORY\x10\x01\x12\x14\n\x10INTERNAL_STORAGE\x10\x02\"\x04\x08\x03\x10\x03*\x10\x45XTERNAL_STORAGEB\x1e\n\x1aorg.finos.tracdap.metadataP\x01\x62\x06proto3')
20
+
21
+ _globals = globals()
22
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
23
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tracdap.rt._impl.grpc.tracdap.metadata.resource_pb2', _globals)
24
+ if _descriptor._USE_C_DESCRIPTORS == False:
25
+ _globals['DESCRIPTOR']._options = None
26
+ _globals['DESCRIPTOR']._serialized_options = b'\n\032org.finos.tracdap.metadataP\001'
27
+ _globals['_RESOURCETYPE']._serialized_start=184
28
+ _globals['_RESOURCETYPE']._serialized_end=293
29
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,16 @@
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 object_pb2 as _object_pb2
3
+ from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
4
+ from google.protobuf import descriptor as _descriptor
5
+ from typing import ClassVar as _ClassVar
6
+
7
+ DESCRIPTOR: _descriptor.FileDescriptor
8
+
9
+ class ResourceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
10
+ __slots__ = ()
11
+ RESOURCE_TYPE_NOT_SET: _ClassVar[ResourceType]
12
+ MODEL_REPOSITORY: _ClassVar[ResourceType]
13
+ INTERNAL_STORAGE: _ClassVar[ResourceType]
14
+ RESOURCE_TYPE_NOT_SET: ResourceType
15
+ MODEL_REPOSITORY: ResourceType
16
+ INTERNAL_STORAGE: ResourceType
@@ -19,6 +19,7 @@ import pathlib
19
19
  import copy
20
20
 
21
21
  import tracdap.rt.api as _api
22
+ import tracdap.rt.api.experimental as _eapi
22
23
  import tracdap.rt.metadata as _meta
23
24
  import tracdap.rt.config as _cfg
24
25
  import tracdap.rt.exceptions as _ex
@@ -208,6 +209,13 @@ class ModelLoader:
208
209
  model_def.inputs = inputs
209
210
  model_def.outputs = outputs
210
211
 
212
+ if isinstance(model, _eapi.TracDataImport):
213
+ model_def.modelType = _meta.ModelType.DATA_IMPORT_MODEL
214
+ elif isinstance(model, _eapi.TracDataExport):
215
+ model_def.modelType = _meta.ModelType.DATA_EXPORT_MODEL
216
+ else:
217
+ model_def.modelType = _meta.ModelType.STANDARD_MODEL
218
+
211
219
  _val.quick_validate_model_def(model_def)
212
220
 
213
221
  for attr_name, attr_value in attributes.items():
@@ -15,8 +15,10 @@
15
15
  import typing as _tp
16
16
  import types as _ts
17
17
 
18
+ import tracdap.rt.api.experimental as _api
18
19
  import tracdap.rt.metadata as _meta
19
20
  import tracdap.rt.exceptions as _ex
21
+ import tracdap.rt._impl.data as _data
20
22
  import tracdap.rt._impl.schemas as _schemas
21
23
  import tracdap.rt._impl.type_system as _type_system
22
24
  import tracdap.rt._impl.validation as _val
@@ -36,6 +38,24 @@ class StaticApiImpl(_StaticApiHook):
36
38
  if not _StaticApiHook._is_registered():
37
39
  _StaticApiHook._register(StaticApiImpl())
38
40
 
41
+ def array_type(self, item_type: _meta.BasicType) -> _meta.TypeDescriptor:
42
+
43
+ _val.validate_signature(self.array_type, item_type)
44
+
45
+ if not _val.is_primitive_type(item_type):
46
+ raise _ex.EModelValidation(f"Arrays can only contain primitive types, [{item_type}] is not primitive")
47
+
48
+ return _meta.TypeDescriptor(_meta.BasicType.ARRAY, arrayType=_meta.TypeDescriptor(item_type))
49
+
50
+ def map_type(self, entry_type: _meta.BasicType) -> _meta.TypeDescriptor:
51
+
52
+ _val.validate_signature(self.map_type, entry_type)
53
+
54
+ if not _val.is_primitive_type(entry_type):
55
+ raise _ex.EModelValidation(f"Maps can only contain primitive types, [{entry_type}] is not primitive")
56
+
57
+ return _meta.TypeDescriptor(_meta.BasicType.MAP, arrayType=_meta.TypeDescriptor(entry_type))
58
+
39
59
  def define_attribute(
40
60
  self, attr_name: str, attr_value: _tp.Any,
41
61
  attr_type: _tp.Optional[_meta.BasicType] = None,
@@ -152,6 +172,15 @@ class StaticApiImpl(_StaticApiHook):
152
172
 
153
173
  return _schemas.SchemaLoader.load_schema(package, schema_file)
154
174
 
175
+ def infer_schema(self, dataset: _api.DATA_API) -> _meta.SchemaDefinition:
176
+
177
+ _val.validate_signature(self.infer_schema, dataset)
178
+
179
+ framework = _data.DataConverter.get_framework(dataset)
180
+ converter = _data.DataConverter.for_framework(framework)
181
+
182
+ return converter.infer_schema(dataset)
183
+
155
184
  def define_input_table(
156
185
  self, *fields: _tp.Union[_meta.FieldSchema, _tp.List[_meta.FieldSchema]],
157
186
  label: _tp.Optional[str] = None, optional: bool = False, dynamic: bool = False,
@@ -30,9 +30,10 @@ import tracdap.rt.exceptions as _ex
30
30
  import tracdap.rt.ext.plugins as plugins
31
31
  import tracdap.rt._impl.data as _data
32
32
  import tracdap.rt._impl.util as _util
33
+ import tracdap.rt._impl.validation as _val
33
34
 
34
- # Import storage interfaces
35
- from tracdap.rt.ext.storage import *
35
+ # Import storage interfaces (using the internal version, it has extra bits that are not public)
36
+ from tracdap.rt._impl.ext.storage import *
36
37
 
37
38
 
38
39
  class FormatManager:
@@ -41,7 +42,11 @@ class FormatManager:
41
42
  def get_data_format(cls, format_code: str, format_options: tp.Dict[str, tp.Any]) -> IDataFormat:
42
43
 
43
44
  try:
44
- config = _cfg.PluginConfig(format_code, format_options)
45
+
46
+ config = _cfg.PluginConfig(
47
+ protocol=format_code,
48
+ properties=format_options)
49
+
45
50
  return plugins.PluginManager.load_plugin(IDataFormat, config)
46
51
 
47
52
  except _ex.EPluginNotAvailable as e:
@@ -73,11 +78,18 @@ class StorageManager:
73
78
  self.__log = _util.logger_for_object(self)
74
79
  self.__file_storage: tp.Dict[str, IFileStorage] = dict()
75
80
  self.__data_storage: tp.Dict[str, IDataStorage] = dict()
81
+ self.__external: tp.List[str] = list()
76
82
  self.__settings = sys_config.storage
77
83
 
78
84
  for storage_key, storage_config in sys_config.storage.buckets.items():
79
85
  self.create_storage(storage_key, storage_config)
80
86
 
87
+ for storage_key, storage_config in sys_config.storage.external.items():
88
+ if storage_key in self.__file_storage or storage_key in self.__data_storage:
89
+ raise _ex.EConfig(f"Storage key [{storage_key}] is defined as both internal and external storage")
90
+ self.__external.append(storage_key)
91
+ self.create_storage(storage_key, storage_config)
92
+
81
93
  def default_storage_key(self):
82
94
  return self.__settings.defaultBucket
83
95
 
@@ -147,26 +159,32 @@ class StorageManager:
147
159
  self.__file_storage[storage_key] = file_storage
148
160
  self.__data_storage[storage_key] = data_storage
149
161
 
150
- def has_file_storage(self, storage_key: str) -> bool:
162
+ def has_file_storage(self, storage_key: str, external: bool = False) -> bool:
163
+
164
+ if external ^ (storage_key in self.__external):
165
+ return False
151
166
 
152
167
  return storage_key in self.__file_storage
153
168
 
154
- def get_file_storage(self, storage_key: str) -> IFileStorage:
169
+ def get_file_storage(self, storage_key: str, external: bool = False) -> IFileStorage:
155
170
 
156
- if not self.has_file_storage(storage_key):
171
+ if not self.has_file_storage(storage_key, external):
157
172
  err = f"File storage is not configured for storage key [{storage_key}]"
158
173
  self.__log.error(err)
159
174
  raise _ex.EStorageConfig(err)
160
175
 
161
176
  return self.__file_storage[storage_key]
162
177
 
163
- def has_data_storage(self, storage_key: str) -> bool:
178
+ def has_data_storage(self, storage_key: str, external: bool = False) -> bool:
179
+
180
+ if external ^ (storage_key in self.__external):
181
+ return False
164
182
 
165
183
  return storage_key in self.__data_storage
166
184
 
167
- def get_data_storage(self, storage_key: str) -> IDataStorage:
185
+ def get_data_storage(self, storage_key: str, external: bool = False) -> IDataStorage:
168
186
 
169
- if not self.has_data_storage(storage_key):
187
+ if not self.has_data_storage(storage_key, external):
170
188
  err = f"Data storage is not configured for storage key [{storage_key}]"
171
189
  self.__log.error(err)
172
190
  raise _ex.EStorageConfig(err)
@@ -587,29 +605,27 @@ class CommonFileStorage(IFileStorage):
587
605
 
588
606
  try:
589
607
 
590
- if storage_path is None or len(storage_path.strip()) == 0:
608
+ if _val.StorageValidator.storage_path_is_empty(storage_path):
591
609
  raise self._explicit_error(self.ExplicitError.STORAGE_PATH_NULL_OR_BLANK, operation_name, storage_path)
592
610
 
593
- if self._ILLEGAL_PATH_CHARS.match(storage_path):
611
+ if _val.StorageValidator.storage_path_invalid(storage_path):
594
612
  raise self._explicit_error(self.ExplicitError.STORAGE_PATH_INVALID, operation_name, storage_path)
595
-
596
- relative_path = pathlib.Path(storage_path)
597
-
598
- if relative_path.is_absolute():
613
+
614
+ if _val.StorageValidator.storage_path_not_relative(storage_path):
599
615
  raise self._explicit_error(self.ExplicitError.STORAGE_PATH_NOT_RELATIVE, operation_name, storage_path)
600
616
 
617
+ if _val.StorageValidator.storage_path_outside_root(storage_path):
618
+ raise self._explicit_error(self.ExplicitError.STORAGE_PATH_OUTSIDE_ROOT, operation_name, storage_path)
619
+
620
+ if not allow_root_dir and _val.StorageValidator.storage_path_is_root(storage_path):
621
+ raise self._explicit_error(self.ExplicitError.STORAGE_PATH_IS_ROOT, operation_name, storage_path)
622
+
601
623
  root_path = pathlib.Path("C:\\root") if _util.is_windows() else pathlib.Path("/root")
624
+ relative_path = pathlib.Path(storage_path)
602
625
  absolute_path = root_path.joinpath(relative_path).resolve(False)
603
626
 
604
627
  if absolute_path == root_path:
605
- if not allow_root_dir:
606
- raise self._explicit_error(self.ExplicitError.STORAGE_PATH_IS_ROOT, operation_name, storage_path)
607
- else:
608
- return ""
609
-
610
- # is_relative_to only supported in Python 3.9+, we need to support 3.7
611
- if root_path not in absolute_path.parents:
612
- raise self._explicit_error(self.ExplicitError.STORAGE_PATH_OUTSIDE_ROOT, operation_name, storage_path)
628
+ return ""
613
629
  else:
614
630
  return absolute_path.relative_to(root_path).as_posix()
615
631
 
@@ -639,10 +655,6 @@ class CommonFileStorage(IFileStorage):
639
655
 
640
656
  return err
641
657
 
642
- _ILLEGAL_PATH_CHARS_WINDOWS = re.compile(r".*[\x00<>:\"\'|?*].*")
643
- _ILLEGAL_PATH_CHARS_POSIX = re.compile(r".*[\x00<>:\"\'|?*\\].*")
644
- _ILLEGAL_PATH_CHARS = _ILLEGAL_PATH_CHARS_WINDOWS if _util.is_windows() else _ILLEGAL_PATH_CHARS_POSIX
645
-
646
658
  class ExplicitError(enum.Enum):
647
659
 
648
660
  # Validation failures
tracdap/rt/_impl/util.py CHANGED
@@ -262,6 +262,16 @@ def get_args(metaclass: type):
262
262
  return None
263
263
 
264
264
 
265
+ def get_constraints(metaclass: tp.TypeVar):
266
+
267
+ return metaclass.__constraints__ or None
268
+
269
+
270
+ def get_bound(metaclass: tp.TypeVar):
271
+
272
+ return metaclass.__bound__ or None
273
+
274
+
265
275
  def try_clean_dir(dir_path: pathlib.Path, remove: bool = False) -> bool:
266
276
 
267
277
  normalized_path = windows_unc_path(dir_path)
@@ -15,7 +15,9 @@
15
15
  import inspect
16
16
  import logging
17
17
  import re
18
+ import types
18
19
  import typing as tp
20
+ import pathlib
19
21
 
20
22
  import tracdap.rt.metadata as meta
21
23
  import tracdap.rt.exceptions as ex
@@ -25,6 +27,11 @@ import tracdap.rt._impl.util as util
25
27
  from tracdap.rt.api.hook import _Named # noqa
26
28
 
27
29
 
30
+ def require_package(module_name: str, module_obj: types.ModuleType):
31
+ if module_obj is None:
32
+ raise ex.ERuntimeValidation(f"Optional package [{module_name}] is not installed")
33
+
34
+
28
35
  def validate_signature(method: tp.Callable, *args, **kwargs):
29
36
  _TypeValidator.validate_signature(method, *args, **kwargs)
30
37
 
@@ -37,10 +44,25 @@ def check_type(expected_type: tp.Type, value: tp.Any) -> bool:
37
44
  return _TypeValidator.check_type(expected_type, value)
38
45
 
39
46
 
47
+ def type_name(type_: tp.Type, qualified: bool) -> str:
48
+ return _TypeValidator._type_name(type_, qualified) # noqa
49
+
50
+
40
51
  def quick_validate_model_def(model_def: meta.ModelDefinition):
41
52
  StaticValidator.quick_validate_model_def(model_def)
42
53
 
43
54
 
55
+ def is_primitive_type(basic_type: meta.BasicType) -> bool:
56
+ return StaticValidator.is_primitive_type(basic_type)
57
+
58
+
59
+ T_SKIP_VAL = tp.TypeVar("T_SKIP_VAL")
60
+
61
+ class SkipValidation(tp.Generic[T_SKIP_VAL]):
62
+ def __init__(self, skip_type: tp.Type[T_SKIP_VAL]):
63
+ self.skip_type = skip_type
64
+
65
+
44
66
  class _TypeValidator:
45
67
 
46
68
  # The metaclass for generic types varies between versions of the typing library
@@ -49,37 +71,42 @@ class _TypeValidator:
49
71
 
50
72
  # Cache method signatures to avoid inspection on every call
51
73
  # Inspecting a function signature can take ~ half a second in Python 3.7
52
- __method_cache: tp.Dict[str, inspect.Signature] = dict()
74
+ __method_cache: tp.Dict[str, tp.Tuple[inspect.Signature, tp.Any]] = dict()
53
75
 
54
76
  _log: logging.Logger = util.logger_for_namespace(__name__)
55
77
 
56
78
  @classmethod
57
79
  def validate_signature(cls, method: tp.Callable, *args, **kwargs):
58
80
 
59
- if method.__name__ in cls.__method_cache:
60
- signature = cls.__method_cache[method.__name__]
81
+ if method.__qualname__ in cls.__method_cache:
82
+ signature, hints = cls.__method_cache[method.__qualname__]
61
83
  else:
62
84
  signature = inspect.signature(method)
63
- cls.__method_cache[method.__name__] = signature
85
+ hints = tp.get_type_hints(method)
86
+ cls.__method_cache[method.__qualname__] = signature, hints
64
87
 
88
+ named_params = list(signature.parameters.keys())
65
89
  positional_index = 0
66
90
 
67
91
  for param_name, param in signature.parameters.items():
68
92
 
69
- values = cls._select_arg(method.__name__, param, positional_index, *args, **kwargs)
93
+ param_type = hints.get(param_name)
94
+
95
+ values = cls._select_arg(method.__name__, param, positional_index, named_params, *args, **kwargs)
70
96
  positional_index += len(values)
71
97
 
72
98
  for value in values:
73
- cls._validate_arg(method.__name__, param, value)
99
+ cls._validate_arg(method.__name__, param_name, param_type, value)
74
100
 
75
101
  @classmethod
76
102
  def validate_return_type(cls, method: tp.Callable, value: tp.Any):
77
103
 
78
- if method.__name__ in cls.__method_cache:
79
- signature = cls.__method_cache[method.__name__]
104
+ if method.__qualname__ in cls.__method_cache:
105
+ signature, hints = cls.__method_cache[method.__qualname__]
80
106
  else:
81
107
  signature = inspect.signature(method)
82
- cls.__method_cache[method.__name__] = signature
108
+ hints = tp.get_type_hints(method)
109
+ cls.__method_cache[method.__qualname__] = signature, hints
83
110
 
84
111
  correct_type = cls._validate_type(signature.return_annotation, value)
85
112
 
@@ -96,7 +123,7 @@ class _TypeValidator:
96
123
 
97
124
  @classmethod
98
125
  def _select_arg(
99
- cls, method_name: str, parameter: inspect.Parameter, positional_index,
126
+ cls, method_name: str, parameter: inspect.Parameter, positional_index, named_params,
100
127
  *args, **kwargs) -> tp.List[tp.Any]:
101
128
 
102
129
  if parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
@@ -141,19 +168,23 @@ class _TypeValidator:
141
168
 
142
169
  if parameter.kind == inspect.Parameter.VAR_KEYWORD:
143
170
 
144
- raise ex.ETracInternal("Validation of VAR_KEYWORD params is not supported yet")
171
+ return [arg for kw, arg in kwargs.items() if kw not in named_params]
145
172
 
146
173
  raise ex.EUnexpected("Invalid method signature in runtime API (this is a bug)")
147
174
 
148
175
  @classmethod
149
- def _validate_arg(cls, method_name: str, parameter: inspect.Parameter, value: tp.Any):
176
+ def _validate_arg(cls, method_name: str, param_name: str, param_type: tp.Type, value: tp.Any):
150
177
 
151
- if not cls._validate_type(parameter.annotation, value):
178
+ if not cls._validate_type(param_type, value):
152
179
 
153
- expected_type = cls._type_name(parameter.annotation)
180
+ expected_type = cls._type_name(param_type)
154
181
  actual_type = cls._type_name(type(value)) if value is not None else str(None)
155
182
 
156
- err = f"Invalid API call [{method_name}()]: Wrong type for [{parameter.name}]" \
183
+ if expected_type == actual_type:
184
+ expected_type = cls._type_name(param_type, qualified=True)
185
+ actual_type = cls._type_name(type(value), qualified=True)
186
+
187
+ err = f"Invalid API call [{method_name}()]: Wrong type for [{param_name}]" \
157
188
  + f" (expected [{expected_type}], got [{actual_type}])"
158
189
 
159
190
  cls._log.error(err)
@@ -165,6 +196,12 @@ class _TypeValidator:
165
196
  if expected_type == tp.Any:
166
197
  return True
167
198
 
199
+ # Sometimes we need to validate a partial set of arguments
200
+ # Explicitly passing a SkipValidation value allows for this
201
+ if isinstance(value, SkipValidation):
202
+ if value.skip_type == expected_type:
203
+ return True
204
+
168
205
  if isinstance(expected_type, cls.__generic_metaclass):
169
206
 
170
207
  origin = util.get_origin(expected_type)
@@ -201,8 +238,31 @@ class _TypeValidator:
201
238
  all(map(lambda k: cls._validate_type(key_type, k), value.keys())) and \
202
239
  all(map(lambda v: cls._validate_type(value_type, v), value.values()))
203
240
 
241
+ if origin.__module__.startswith("tracdap.rt.api."):
242
+ return isinstance(value, origin)
243
+
204
244
  raise ex.ETracInternal(f"Validation of [{origin.__name__}] generic parameters is not supported yet")
205
245
 
246
+ # Support for generic type variables
247
+ if isinstance(expected_type, tp.TypeVar):
248
+
249
+ # If there are any constraints or a bound, those must be honoured
250
+
251
+ constraints = util.get_constraints(expected_type)
252
+ bound = util.get_bound(expected_type)
253
+
254
+ if constraints:
255
+ if not any(map(lambda c: type(value) == c, constraints)):
256
+ return False
257
+
258
+ if bound:
259
+ if not isinstance(value, bound):
260
+ return False
261
+
262
+ # So long as constraints / bound are ok, any type matches a generic type var
263
+ return True
264
+
265
+
206
266
  # Validate everything else as a concrete type
207
267
 
208
268
  # TODO: Recursive validation of types for class members using field annotations
@@ -210,7 +270,7 @@ class _TypeValidator:
210
270
  return isinstance(value, expected_type)
211
271
 
212
272
  @classmethod
213
- def _type_name(cls, type_var: tp.Type) -> str:
273
+ def _type_name(cls, type_var: tp.Type, qualified: bool = False) -> str:
214
274
 
215
275
  if isinstance(type_var, cls.__generic_metaclass):
216
276
 
@@ -222,7 +282,10 @@ class _TypeValidator:
222
282
  return f"Named[{named_type}]"
223
283
 
224
284
  if origin is tp.Union:
225
- return "|".join(map(cls._type_name, args))
285
+ if len(args) == 2 and args[1] == type(None):
286
+ return f"Optional[{cls._type_name(args[0])}]"
287
+ else:
288
+ return "|".join(map(cls._type_name, args))
226
289
 
227
290
  if origin is list:
228
291
  list_type = cls._type_name(args[0])
@@ -230,7 +293,10 @@ class _TypeValidator:
230
293
 
231
294
  raise ex.ETracInternal(f"Validation of [{origin.__name__}] generic parameters is not supported yet")
232
295
 
233
- return type_var.__name__
296
+ if qualified:
297
+ return f"{type_var.__module__}.{type_var.__name__}"
298
+ else:
299
+ return type_var.__name__
234
300
 
235
301
 
236
302
  class StaticValidator:
@@ -256,6 +322,11 @@ class StaticValidator:
256
322
 
257
323
  _log: logging.Logger = util.logger_for_namespace(__name__)
258
324
 
325
+ @classmethod
326
+ def is_primitive_type(cls, basic_type: meta.BasicType) -> bool:
327
+
328
+ return basic_type in cls.__PRIMITIVE_TYPES
329
+
259
330
  @classmethod
260
331
  def quick_validate_model_def(cls, model_def: meta.ModelDefinition):
261
332
 
@@ -458,3 +529,54 @@ class StaticValidator:
458
529
  def _fail(cls, message: str):
459
530
  cls._log.error(message)
460
531
  raise ex.EModelValidation(message)
532
+
533
+
534
+ class StorageValidator:
535
+
536
+ __ILLEGAL_PATH_CHARS_WINDOWS = re.compile(r".*[\x00<>:\"\'|?*].*")
537
+ __ILLEGAL_PATH_CHARS_POSIX = re.compile(r".*[\x00<>:\"\'|?*\\].*")
538
+ __ILLEGAL_PATH_CHARS = __ILLEGAL_PATH_CHARS_WINDOWS if util.is_windows() else __ILLEGAL_PATH_CHARS_POSIX
539
+
540
+ @classmethod
541
+ def storage_path_is_empty(cls, storage_path: str):
542
+
543
+ return storage_path is None or len(storage_path.strip()) == 0
544
+
545
+ @classmethod
546
+ def storage_path_invalid(cls, storage_path: str):
547
+
548
+ if cls.__ILLEGAL_PATH_CHARS.match(storage_path):
549
+ return True
550
+
551
+ try:
552
+ # Make sure the path can be interpreted as a path
553
+ pathlib.Path(storage_path)
554
+ return False
555
+ except ValueError:
556
+ return True
557
+
558
+ @classmethod
559
+ def storage_path_not_relative(cls, storage_path: str):
560
+
561
+ relative_path = pathlib.Path(storage_path)
562
+ return relative_path.is_absolute()
563
+
564
+ @classmethod
565
+ def storage_path_outside_root(cls, storage_path: str):
566
+
567
+ # is_relative_to only supported in Python 3.9+, we need to support 3.8
568
+
569
+ root_path = pathlib.Path("C:\\root") if util.is_windows() else pathlib.Path("/root")
570
+ relative_path = pathlib.Path(storage_path)
571
+ absolute_path = root_path.joinpath(relative_path).resolve(False)
572
+
573
+ return root_path != absolute_path and root_path not in absolute_path.parents
574
+
575
+ @classmethod
576
+ def storage_path_is_root(cls, storage_path: str):
577
+
578
+ root_path = pathlib.Path("C:\\root") if util.is_windows() else pathlib.Path("/root")
579
+ relative_path = pathlib.Path(storage_path)
580
+ absolute_path = root_path.joinpath(relative_path).resolve(False)
581
+
582
+ return root_path == absolute_path
@@ -38,7 +38,7 @@ class GitRepository(IModelRepository):
38
38
 
39
39
  REPO_URL_KEY = "repoUrl"
40
40
  NATIVE_GIT_KEY = "nativeGit"
41
- NATIVE_GIT_DEFAULT = True
41
+ NATIVE_GIT_DEFAULT = False
42
42
 
43
43
  GIT_TIMEOUT_SECONDS = 30
44
44