tracdap-runtime 0.6.4__py3-none-any.whl → 0.6.5__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 (33) hide show
  1. tracdap/rt/_exec/context.py +382 -29
  2. tracdap/rt/_exec/dev_mode.py +123 -94
  3. tracdap/rt/_exec/engine.py +120 -9
  4. tracdap/rt/_exec/functions.py +125 -20
  5. tracdap/rt/_exec/graph.py +38 -13
  6. tracdap/rt/_exec/graph_builder.py +120 -9
  7. tracdap/rt/_impl/data.py +115 -49
  8. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +74 -30
  9. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +120 -2
  10. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +12 -10
  11. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.pyi +14 -2
  12. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +29 -0
  13. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.pyi +16 -0
  14. tracdap/rt/_impl/models.py +8 -0
  15. tracdap/rt/_impl/static_api.py +16 -0
  16. tracdap/rt/_impl/storage.py +37 -25
  17. tracdap/rt/_impl/validation.py +76 -7
  18. tracdap/rt/_plugins/repo_git.py +1 -1
  19. tracdap/rt/_version.py +1 -1
  20. tracdap/rt/api/experimental.py +220 -0
  21. tracdap/rt/api/hook.py +4 -0
  22. tracdap/rt/api/model_api.py +48 -6
  23. tracdap/rt/config/__init__.py +2 -2
  24. tracdap/rt/config/common.py +6 -0
  25. tracdap/rt/metadata/__init__.py +25 -20
  26. tracdap/rt/metadata/job.py +54 -0
  27. tracdap/rt/metadata/model.py +18 -0
  28. tracdap/rt/metadata/resource.py +24 -0
  29. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/METADATA +3 -1
  30. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/RECORD +33 -29
  31. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/LICENSE +0 -0
  32. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/WHEEL +0 -0
  33. {tracdap_runtime-0.6.4.dist-info → tracdap_runtime-0.6.5.dist-info}/top_level.txt +0 -0
@@ -19,13 +19,13 @@ import typing as tp
19
19
  import re
20
20
  import traceback
21
21
 
22
- import pandas as pd
23
-
24
22
  import tracdap.rt.api as _api
23
+ import tracdap.rt.api.experimental as _eapi
25
24
  import tracdap.rt.metadata as _meta
26
25
  import tracdap.rt.exceptions as _ex
27
26
  import tracdap.rt._impl.type_system as _types # noqa
28
27
  import tracdap.rt._impl.data as _data # noqa
28
+ import tracdap.rt._impl.storage as _storage # noqa
29
29
  import tracdap.rt._impl.util as _util # noqa
30
30
  import tracdap.rt._impl.validation as _val # noqa
31
31
 
@@ -61,6 +61,7 @@ class TracContextImpl(_api.TracContext):
61
61
  model_def: _meta.ModelDefinition,
62
62
  model_class: _api.TracModel.__class__,
63
63
  local_ctx: tp.Dict[str, tp.Any],
64
+ dynamic_outputs: tp.List[str] = None,
64
65
  checkout_directory: pathlib.Path = None):
65
66
 
66
67
  self.__ctx_log = _util.logger_for_object(self)
@@ -68,12 +69,14 @@ class TracContextImpl(_api.TracContext):
68
69
 
69
70
  self.__model_def = model_def
70
71
  self.__model_class = model_class
71
- self.__local_ctx = local_ctx or {}
72
+ self.__local_ctx = local_ctx if local_ctx is not None else {}
73
+ self.__dynamic_outputs = dynamic_outputs if dynamic_outputs is not None else []
72
74
 
73
75
  self.__val = TracContextValidator(
74
76
  self.__ctx_log,
75
77
  self.__model_def,
76
78
  self.__local_ctx,
79
+ self.__dynamic_outputs,
77
80
  checkout_directory)
78
81
 
79
82
  def get_parameter(self, parameter_name: str) -> tp.Any:
@@ -131,10 +134,45 @@ class TracContextImpl(_api.TracContext):
131
134
  else:
132
135
  return copy.deepcopy(data_view.trac_schema)
133
136
 
134
- def get_pandas_table(self, dataset_name: str, use_temporal_objects: tp.Optional[bool] = None) -> pd.DataFrame:
137
+ def get_table(self, dataset_name: str, framework, **kwargs) -> _eapi._DATA_FRAMEWORK: # noqa
138
+
139
+ # Support the experimental API data framework syntax
140
+
141
+ if framework == _eapi.PANDAS:
142
+ return self.get_pandas_table(dataset_name, **kwargs)
143
+ elif framework == _eapi.POLARS:
144
+ return self.get_polars_table(dataset_name)
145
+ else:
146
+ raise _ex.ERuntimeValidation(f"Unsupported data framework [{framework}]")
147
+
148
+ def get_pandas_table(self, dataset_name: str, use_temporal_objects: tp.Optional[bool] = None) \
149
+ -> "_data.pandas.DataFrame":
135
150
 
151
+ _val.require_package("pandas", _data.pandas)
136
152
  _val.validate_signature(self.get_pandas_table, dataset_name, use_temporal_objects)
137
153
 
154
+ data_view, schema = self.__get_data_view(dataset_name)
155
+ part_key = _data.DataPartKey.for_root()
156
+
157
+ if use_temporal_objects is None:
158
+ use_temporal_objects = self.__DEFAULT_TEMPORAL_OBJECTS
159
+
160
+ return _data.DataMapping.view_to_pandas(data_view, part_key, schema, use_temporal_objects)
161
+
162
+ def get_polars_table(self, dataset_name: str) -> "_data.polars.DataFrame":
163
+
164
+ _val.require_package("polars", _data.polars)
165
+ _val.validate_signature(self.get_polars_table, dataset_name)
166
+
167
+ data_view, schema = self.__get_data_view(dataset_name)
168
+ part_key = _data.DataPartKey.for_root()
169
+
170
+ return _data.DataMapping.view_to_polars(data_view, part_key, schema)
171
+
172
+ def __get_data_view(self, dataset_name: str):
173
+
174
+ _val.validate_signature(self.__get_data_view, dataset_name)
175
+
138
176
  self.__val.check_dataset_valid_identifier(dataset_name)
139
177
  self.__val.check_dataset_defined_in_model(dataset_name)
140
178
  self.__val.check_dataset_available_in_context(dataset_name)
@@ -155,10 +193,7 @@ class TracContextImpl(_api.TracContext):
155
193
  else:
156
194
  schema = data_view.arrow_schema
157
195
 
158
- if use_temporal_objects is None:
159
- use_temporal_objects = self.__DEFAULT_TEMPORAL_OBJECTS
160
-
161
- return _data.DataMapping.view_to_pandas(data_view, part_key, schema, use_temporal_objects)
196
+ return data_view, schema
162
197
 
163
198
  def put_schema(self, dataset_name: str, schema: _meta.SchemaDefinition):
164
199
 
@@ -190,17 +225,57 @@ class TracContextImpl(_api.TracContext):
190
225
 
191
226
  self.__local_ctx[dataset_name] = updated_view
192
227
 
193
- def put_pandas_table(self, dataset_name: str, dataset: pd.DataFrame):
228
+ def put_table(self, dataset_name: str, dataset: _eapi._DATA_FRAMEWORK, **kwargs): # noqa
229
+
230
+ # Support the experimental API data framework syntax
194
231
 
232
+ if _data.pandas and isinstance(dataset, _data.pandas.DataFrame):
233
+ self.put_pandas_table(dataset_name, dataset)
234
+ elif _data.polars and isinstance(dataset, _data.polars.DataFrame):
235
+ self.put_polars_table(dataset_name, dataset)
236
+ else:
237
+ raise _ex.ERuntimeValidation(f"Unsupported data framework[{type(dataset)}]")
238
+
239
+ def put_pandas_table(self, dataset_name: str, dataset: "_data.pandas.DataFrame"):
240
+
241
+ _val.require_package("pandas", _data.pandas)
195
242
  _val.validate_signature(self.put_pandas_table, dataset_name, dataset)
196
243
 
244
+ part_key = _data.DataPartKey.for_root()
245
+ data_view, schema = self.__put_data_view(dataset_name, part_key, dataset, _data.pandas.DataFrame)
246
+
247
+ # Data conformance is applied inside these conversion functions
248
+
249
+ updated_item = _data.DataMapping.pandas_to_item(dataset, schema)
250
+ updated_view = _data.DataMapping.add_item_to_view(data_view, part_key, updated_item)
251
+
252
+ self.__local_ctx[dataset_name] = updated_view
253
+
254
+ def put_polars_table(self, dataset_name: str, dataset: "_data.polars.DataFrame"):
255
+
256
+ _val.require_package("polars", _data.polars)
257
+ _val.validate_signature(self.put_polars_table, dataset_name, dataset)
258
+
259
+ part_key = _data.DataPartKey.for_root()
260
+ data_view, schema = self.__put_data_view(dataset_name, part_key, dataset, _data.polars.DataFrame)
261
+
262
+ # Data conformance is applied inside these conversion functions
263
+
264
+ updated_item = _data.DataMapping.polars_to_item(dataset, schema)
265
+ updated_view = _data.DataMapping.add_item_to_view(data_view, part_key, updated_item)
266
+
267
+ self.__local_ctx[dataset_name] = updated_view
268
+
269
+ def __put_data_view(self, dataset_name: str, part_key: _data.DataPartKey, dataset: tp.Any, framework: type):
270
+
271
+ _val.validate_signature(self.__put_data_view, dataset_name, part_key, dataset, framework)
272
+
197
273
  self.__val.check_dataset_valid_identifier(dataset_name)
198
274
  self.__val.check_dataset_is_model_output(dataset_name)
199
- self.__val.check_provided_dataset_type(dataset, pd.DataFrame)
275
+ self.__val.check_provided_dataset_type(dataset, framework)
200
276
 
201
277
  static_schema = self.__get_static_schema(self.__model_def, dataset_name)
202
278
  data_view = self.__local_ctx.get(dataset_name)
203
- part_key = _data.DataPartKey.for_root()
204
279
 
205
280
  if data_view is None:
206
281
  if static_schema is not None:
@@ -219,12 +294,7 @@ class TracContextImpl(_api.TracContext):
219
294
  else:
220
295
  schema = data_view.arrow_schema
221
296
 
222
- # Data conformance is applied inside these conversion functions
223
-
224
- updated_item = _data.DataMapping.pandas_to_item(dataset, schema)
225
- updated_view = _data.DataMapping.add_item_to_view(data_view, part_key, updated_item)
226
-
227
- self.__local_ctx[dataset_name] = updated_view
297
+ return data_view, schema
228
298
 
229
299
  def log(self) -> logging.Logger:
230
300
 
@@ -260,20 +330,212 @@ class TracContextImpl(_api.TracContext):
260
330
  return schema_def
261
331
 
262
332
 
263
- class TracContextValidator:
264
-
265
- __VALID_IDENTIFIER = re.compile("^[a-zA-Z_]\\w*$",)
266
- __RESERVED_IDENTIFIER = re.compile("^(trac_|_)\\w*")
333
+ class TracDataContextImpl(TracContextImpl, _eapi.TracDataContext):
267
334
 
268
335
  def __init__(
269
- self, log: logging.Logger,
270
- model_def: _meta.ModelDefinition,
271
- local_ctx: tp.Dict[str, tp.Any],
272
- checkout_directory: pathlib.Path):
336
+ self, model_def: _meta.ModelDefinition, model_class: _api.TracModel.__class__,
337
+ local_ctx: tp.Dict[str, tp.Any], dynamic_outputs: tp.List[str],
338
+ storage_map: tp.Dict[str, tp.Union[_eapi.TracFileStorage]],
339
+ checkout_directory: pathlib.Path = None):
340
+
341
+ super().__init__(model_def, model_class, local_ctx, dynamic_outputs, checkout_directory)
273
342
 
274
- self.__log = log
275
343
  self.__model_def = model_def
276
344
  self.__local_ctx = local_ctx
345
+ self.__dynamic_outputs = dynamic_outputs
346
+ self.__storage_map = storage_map
347
+ self.__checkout_directory = checkout_directory
348
+
349
+ self.__val = self._TracContextImpl__val # noqa
350
+
351
+ def get_file_storage(self, storage_key: str) -> _eapi.TracFileStorage:
352
+
353
+ _val.validate_signature(self.get_file_storage, storage_key)
354
+
355
+ self.__val.check_storage_valid_identifier(storage_key)
356
+ self.__val.check_storage_available(self.__storage_map, storage_key)
357
+ self.__val.check_storage_type(self.__storage_map, storage_key, _eapi.TracFileStorage)
358
+
359
+ return self.__storage_map[storage_key]
360
+
361
+ def get_data_storage(self, storage_key: str) -> None:
362
+ raise _ex.ERuntimeValidation("Data storage API not available yet")
363
+
364
+ def add_data_import(self, dataset_name: str):
365
+
366
+ _val.validate_signature(self.add_data_import, dataset_name)
367
+
368
+ self.__val.check_dataset_valid_identifier(dataset_name)
369
+ self.__val.check_dataset_not_defined_in_model(dataset_name)
370
+ self.__val.check_dataset_not_available_in_context(dataset_name)
371
+
372
+ self.__local_ctx[dataset_name] = _data.DataView.create_empty()
373
+ self.__dynamic_outputs.append(dataset_name)
374
+
375
+ def set_source_metadata(self, dataset_name: str, storage_key: str, source_info: _eapi.FileStat):
376
+
377
+ _val.validate_signature(self.add_data_import, dataset_name, storage_key, source_info)
378
+
379
+ pass # Not implemented yet, only required when imports are sent back to the platform
380
+
381
+ def set_attribute(self, dataset_name: str, attribute_name: str, value: tp.Any):
382
+
383
+ _val.validate_signature(self.add_data_import, dataset_name, attribute_name, value)
384
+
385
+ pass # Not implemented yet, only required when imports are sent back to the platform
386
+
387
+ def set_schema(self, dataset_name: str, schema: _meta.SchemaDefinition):
388
+
389
+ _val.validate_signature(self.set_schema, dataset_name, schema)
390
+
391
+ # Forward to existing method (these should be swapped round)
392
+ self.put_schema(dataset_name, schema)
393
+
394
+
395
+ class TracFileStorageImpl(_eapi.TracFileStorage):
396
+
397
+ def __init__(self, storage_key: str, storage_impl: _storage.IFileStorage, write_access: bool, checkout_directory):
398
+
399
+ self.__storage_key = storage_key
400
+
401
+ self.__exists = lambda sp: storage_impl.exists(sp)
402
+ self.__size = lambda sp: storage_impl.size(sp)
403
+ self.__stat = lambda sp: storage_impl.stat(sp)
404
+ self.__ls = lambda sp, rec: storage_impl.ls(sp, rec)
405
+ self.__read_byte_stream = lambda sp: storage_impl.read_byte_stream(sp)
406
+
407
+ if write_access:
408
+ self.__mkdir = lambda sp, rec: storage_impl.mkdir(sp, rec)
409
+ self.__rm = lambda sp: storage_impl.rm(sp)
410
+ self.__rmdir = lambda sp: storage_impl.rmdir(sp)
411
+ self.__write_byte_stream = lambda sp: storage_impl.write_byte_stream(sp)
412
+ else:
413
+ self.__mkdir = None
414
+ self.__rm = None
415
+ self.__rmdir = None
416
+ self.__write_byte_stream = None
417
+
418
+ self.__log = _util.logger_for_object(self)
419
+ self.__val = TracStorageValidator(self.__log, checkout_directory, self.__storage_key)
420
+
421
+ def get_storage_key(self) -> str:
422
+
423
+ _val.validate_signature(self.get_storage_key)
424
+
425
+ return self.__storage_key
426
+
427
+ def exists(self, storage_path: str) -> bool:
428
+
429
+ _val.validate_signature(self.exists, storage_path)
430
+
431
+ self.__val.check_operation_available(self.exists, self.__exists)
432
+ self.__val.check_storage_path_is_valid(storage_path)
433
+
434
+ return self.__exists(storage_path)
435
+
436
+ def size(self, storage_path: str) -> int:
437
+
438
+ _val.validate_signature(self.size, storage_path)
439
+
440
+ self.__val.check_operation_available(self.size, self.__size)
441
+ self.__val.check_storage_path_is_valid(storage_path)
442
+
443
+ return self.__size(storage_path)
444
+
445
+ def stat(self, storage_path: str) -> _eapi.FileStat:
446
+
447
+ _val.validate_signature(self.stat, storage_path)
448
+
449
+ self.__val.check_operation_available(self.stat, self.__stat)
450
+ self.__val.check_storage_path_is_valid(storage_path)
451
+
452
+ stat = self.__stat(storage_path)
453
+ return _eapi.FileStat(**stat.__dict__)
454
+
455
+ def ls(self, storage_path: str, recursive: bool = False) -> tp.List[_eapi.FileStat]:
456
+
457
+ _val.validate_signature(self.ls, storage_path, recursive)
458
+
459
+ self.__val.check_operation_available(self.ls, self.__ls)
460
+ self.__val.check_storage_path_is_valid(storage_path)
461
+
462
+ listing = self.__ls(storage_path, recursive)
463
+ return list(_eapi.FileStat(**stat.__dict__) for stat in listing)
464
+
465
+ def mkdir(self, storage_path: str, recursive: bool = False):
466
+
467
+ _val.validate_signature(self.mkdir, storage_path, recursive)
468
+
469
+ self.__val.check_operation_available(self.mkdir, self.__mkdir)
470
+ self.__val.check_storage_path_is_valid(storage_path)
471
+ self.__val.check_storage_path_is_not_root(storage_path)
472
+
473
+ self.__mkdir(storage_path, recursive)
474
+
475
+ def rm(self, storage_path: str):
476
+
477
+ _val.validate_signature(self.rm, storage_path)
478
+
479
+ self.__val.check_operation_available(self.rm, self.__rm)
480
+ self.__val.check_storage_path_is_valid(storage_path)
481
+ self.__val.check_storage_path_is_not_root(storage_path)
482
+
483
+ self.__rm(storage_path)
484
+
485
+ def rmdir(self, storage_path: str):
486
+
487
+ _val.validate_signature(self.rmdir, storage_path)
488
+
489
+ self.__val.check_operation_available(self.rmdir, self.__rmdir)
490
+ self.__val.check_storage_path_is_valid(storage_path)
491
+ self.__val.check_storage_path_is_not_root(storage_path)
492
+
493
+ self.__rmdir(storage_path)
494
+
495
+ def read_byte_stream(self, storage_path: str) -> tp.ContextManager[tp.BinaryIO]:
496
+
497
+ _val.validate_signature(self.read_byte_stream, storage_path)
498
+
499
+ self.__val.check_operation_available(self.read_byte_stream, self.__read_byte_stream)
500
+ self.__val.check_storage_path_is_valid(storage_path)
501
+
502
+ return self.__read_byte_stream(storage_path)
503
+
504
+ def read_bytes(self, storage_path: str) -> bytes:
505
+
506
+ _val.validate_signature(self.read_bytes, storage_path)
507
+
508
+ self.__val.check_operation_available(self.read_bytes, self.__read_byte_stream)
509
+ self.__val.check_storage_path_is_valid(storage_path)
510
+
511
+ return super().read_bytes(storage_path)
512
+
513
+ def write_byte_stream(self, storage_path: str) -> tp.ContextManager[tp.BinaryIO]:
514
+
515
+ _val.validate_signature(self.write_byte_stream, storage_path)
516
+
517
+ self.__val.check_operation_available(self.write_byte_stream, self.__write_byte_stream)
518
+ self.__val.check_storage_path_is_valid(storage_path)
519
+ self.__val.check_storage_path_is_not_root(storage_path)
520
+
521
+ return self.__write_byte_stream(storage_path)
522
+
523
+ def write_bytes(self, storage_path: str, data: bytes):
524
+
525
+ _val.validate_signature(self.write_bytes, storage_path)
526
+
527
+ self.__val.check_operation_available(self.write_bytes, self.__write_byte_stream)
528
+ self.__val.check_storage_path_is_valid(storage_path)
529
+ self.__val.check_storage_path_is_not_root(storage_path)
530
+
531
+ super().write_bytes(storage_path, data)
532
+
533
+
534
+ class TracContextErrorReporter:
535
+
536
+ def __init__(self, log: logging.Logger, checkout_directory: pathlib.Path):
537
+
538
+ self.__log = log
277
539
  self.__checkout_directory = checkout_directory
278
540
 
279
541
  def _report_error(self, message, cause: Exception = None):
@@ -292,6 +554,25 @@ class TracContextValidator:
292
554
  else:
293
555
  raise _ex.ERuntimeValidation(message)
294
556
 
557
+
558
+ class TracContextValidator(TracContextErrorReporter):
559
+
560
+ __VALID_IDENTIFIER = re.compile("^[a-zA-Z_]\\w*$",)
561
+ __RESERVED_IDENTIFIER = re.compile("^(trac_|_)\\w*")
562
+
563
+ def __init__(
564
+ self, log: logging.Logger,
565
+ model_def: _meta.ModelDefinition,
566
+ local_ctx: tp.Dict[str, tp.Any],
567
+ dynamic_outputs: tp.List[str],
568
+ checkout_directory: pathlib.Path):
569
+
570
+ super().__init__(log, checkout_directory)
571
+
572
+ self.__model_def = model_def
573
+ self.__local_ctx = local_ctx
574
+ self.__dynamic_outputs = dynamic_outputs
575
+
295
576
  def check_param_valid_identifier(self, param_name: str):
296
577
 
297
578
  if param_name is None:
@@ -318,6 +599,14 @@ class TracContextValidator:
318
599
  if not self.__VALID_IDENTIFIER.match(dataset_name):
319
600
  self._report_error(f"Dataset name {dataset_name} is not a valid identifier")
320
601
 
602
+ def check_dataset_not_defined_in_model(self, dataset_name: str):
603
+
604
+ if dataset_name in self.__model_def.inputs or dataset_name in self.__model_def.outputs:
605
+ self._report_error(f"Dataset {dataset_name} is already defined in the model")
606
+
607
+ if dataset_name in self.__model_def.parameters:
608
+ self._report_error(f"Dataset name {dataset_name} is already in use as a model parameter")
609
+
321
610
  def check_dataset_defined_in_model(self, dataset_name: str):
322
611
 
323
612
  if dataset_name not in self.__model_def.inputs and dataset_name not in self.__model_def.outputs:
@@ -325,17 +614,18 @@ class TracContextValidator:
325
614
 
326
615
  def check_dataset_is_model_output(self, dataset_name: str):
327
616
 
328
- if dataset_name not in self.__model_def.outputs:
617
+ if dataset_name not in self.__model_def.outputs and dataset_name not in self.__dynamic_outputs:
329
618
  self._report_error(f"Dataset {dataset_name} is not defined as a model output")
330
619
 
331
620
  def check_dataset_is_dynamic_output(self, dataset_name: str):
332
621
 
333
622
  model_output: _meta.ModelOutputSchema = self.__model_def.outputs.get(dataset_name)
623
+ dynamic_output = dataset_name in self.__dynamic_outputs
334
624
 
335
- if model_output is None:
625
+ if model_output is None and not dynamic_output:
336
626
  self._report_error(f"Dataset {dataset_name} is not defined as a model output")
337
627
 
338
- if not model_output.dynamic:
628
+ if model_output and not model_output.dynamic:
339
629
  self._report_error(f"Model output {dataset_name} is not a dynamic output")
340
630
 
341
631
  def check_dataset_available_in_context(self, item_name: str):
@@ -343,6 +633,11 @@ class TracContextValidator:
343
633
  if item_name not in self.__local_ctx:
344
634
  self._report_error(f"Dataset {item_name} is not available in the current context")
345
635
 
636
+ def check_dataset_not_available_in_context(self, item_name: str):
637
+
638
+ if item_name in self.__local_ctx:
639
+ self._report_error(f"Dataset {item_name} already exists in the current context")
640
+
346
641
  def check_dataset_schema_defined(self, dataset_name: str, data_view: _data.DataView):
347
642
 
348
643
  schema = data_view.trac_schema if data_view is not None else None
@@ -415,6 +710,33 @@ class TracContextValidator:
415
710
  f"The object referenced by [{item_name}] in the current context has the wrong type" +
416
711
  f" (expected {expected_type_name}, got {actual_type_name})")
417
712
 
713
+ def check_storage_valid_identifier(self, storage_key):
714
+
715
+ if storage_key is None:
716
+ self._report_error(f"Storage key is null")
717
+
718
+ if not self.__VALID_IDENTIFIER.match(storage_key):
719
+ self._report_error(f"Storage key {storage_key} is not a valid identifier")
720
+
721
+ def check_storage_available(self, storage_map: tp.Dict, storage_key: str):
722
+
723
+ storage_instance = storage_map.get(storage_key)
724
+
725
+ if storage_instance is None:
726
+ self._report_error(f"Storage not available for storage key [{storage_key}]")
727
+
728
+ def check_storage_type(
729
+ self, storage_map: tp.Dict, storage_key: str,
730
+ storage_type: tp.Union[_eapi.TracFileStorage.__class__]):
731
+
732
+ storage_instance = storage_map.get(storage_key)
733
+
734
+ if not isinstance(storage_instance, storage_type):
735
+ if storage_type == _eapi.TracFileStorage:
736
+ self._report_error(f"Storage key [{storage_key}] refers to data storage, not file storage")
737
+ else:
738
+ self._report_error(f"Storage key [{storage_key}] refers to file storage, not data storage")
739
+
418
740
  @staticmethod
419
741
  def _type_name(type_: type):
420
742
 
@@ -424,3 +746,34 @@ class TracContextValidator:
424
746
  return type_.__qualname__
425
747
 
426
748
  return module + '.' + type_.__name__
749
+
750
+
751
+ class TracStorageValidator(TracContextErrorReporter):
752
+
753
+ def __init__(self, log, checkout_directory, storage_key):
754
+ super().__init__(log, checkout_directory)
755
+ self.__storage_key = storage_key
756
+
757
+ def check_operation_available(self, public_func: tp.Callable, impl_func: tp.Callable):
758
+
759
+ if impl_func is None:
760
+ self._report_error(f"Operation [{public_func.__name__}] is not available for storage [{self.__storage_key}]")
761
+
762
+ def check_storage_path_is_valid(self, storage_path: str):
763
+
764
+ if _val.StorageValidator.storage_path_is_empty(storage_path):
765
+ self._report_error(f"Storage path is None or empty")
766
+
767
+ if _val.StorageValidator.storage_path_invalid(storage_path):
768
+ self._report_error(f"Storage path [{storage_path}] contains invalid characters")
769
+
770
+ if _val.StorageValidator.storage_path_not_relative(storage_path):
771
+ self._report_error(f"Storage path [{storage_path}] is not a relative path")
772
+
773
+ if _val.StorageValidator.storage_path_outside_root(storage_path):
774
+ self._report_error(f"Storage path [{storage_path}] is outside the storage root")
775
+
776
+ def check_storage_path_is_not_root(self, storage_path: str):
777
+
778
+ if _val.StorageValidator.storage_path_is_empty(storage_path):
779
+ self._report_error(f"Storage path [{storage_path}] is not allowed")