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.
- tracdap/rt/_exec/context.py +556 -36
- tracdap/rt/_exec/dev_mode.py +320 -198
- tracdap/rt/_exec/engine.py +331 -62
- tracdap/rt/_exec/functions.py +151 -22
- tracdap/rt/_exec/graph.py +47 -13
- tracdap/rt/_exec/graph_builder.py +383 -175
- tracdap/rt/_exec/runtime.py +7 -5
- tracdap/rt/_impl/config_parser.py +11 -4
- tracdap/rt/_impl/data.py +329 -152
- tracdap/rt/_impl/ext/__init__.py +13 -0
- tracdap/rt/_impl/ext/sql.py +116 -0
- tracdap/rt/_impl/ext/storage.py +57 -0
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +82 -30
- tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +155 -2
- tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +12 -10
- tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -2
- tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +29 -0
- tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.pyi +16 -0
- tracdap/rt/_impl/models.py +8 -0
- tracdap/rt/_impl/static_api.py +29 -0
- tracdap/rt/_impl/storage.py +39 -27
- tracdap/rt/_impl/util.py +10 -0
- tracdap/rt/_impl/validation.py +140 -18
- tracdap/rt/_plugins/repo_git.py +1 -1
- tracdap/rt/_plugins/storage_sql.py +417 -0
- tracdap/rt/_plugins/storage_sql_dialects.py +117 -0
- tracdap/rt/_version.py +1 -1
- tracdap/rt/api/experimental.py +267 -0
- tracdap/rt/api/hook.py +14 -0
- tracdap/rt/api/model_api.py +48 -6
- tracdap/rt/config/__init__.py +2 -2
- tracdap/rt/config/common.py +6 -0
- tracdap/rt/metadata/__init__.py +29 -20
- tracdap/rt/metadata/job.py +99 -0
- tracdap/rt/metadata/model.py +18 -0
- tracdap/rt/metadata/resource.py +24 -0
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/METADATA +5 -1
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/RECORD +41 -32
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/WHEEL +1 -1
- {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.6.dist-info}/LICENSE +0 -0
- {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
|
tracdap/rt/_impl/models.py
CHANGED
@@ -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():
|
tracdap/rt/_impl/static_api.py
CHANGED
@@ -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,
|
tracdap/rt/_impl/storage.py
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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)
|
tracdap/rt/_impl/validation.py
CHANGED
@@ -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.
|
60
|
-
signature = cls.__method_cache[method.
|
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
|
-
|
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
|
-
|
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__,
|
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.
|
79
|
-
signature = cls.__method_cache[method.
|
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
|
-
|
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
|
-
|
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,
|
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(
|
178
|
+
if not cls._validate_type(param_type, value):
|
152
179
|
|
153
|
-
expected_type = cls._type_name(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|