isar 1.20.2__py3-none-any.whl → 1.34.9__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 (124) hide show
  1. isar/apis/api.py +148 -76
  2. isar/apis/models/__init__.py +0 -1
  3. isar/apis/models/models.py +21 -11
  4. isar/apis/models/start_mission_definition.py +110 -168
  5. isar/apis/robot_control/robot_controller.py +41 -0
  6. isar/apis/schedule/scheduling_controller.py +124 -162
  7. isar/apis/security/authentication.py +5 -5
  8. isar/config/certs/ca-cert.pem +33 -31
  9. isar/config/keyvault/keyvault_service.py +1 -1
  10. isar/config/log.py +45 -40
  11. isar/config/logging.conf +16 -31
  12. isar/config/open_telemetry.py +102 -0
  13. isar/config/predefined_mission_definition/default_exr.json +0 -2
  14. isar/config/predefined_mission_definition/default_mission.json +1 -5
  15. isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
  16. isar/config/predefined_missions/default.json +67 -87
  17. isar/config/predefined_missions/default_extra_capabilities.json +107 -0
  18. isar/config/settings.py +76 -111
  19. isar/eventhandlers/eventhandler.py +123 -0
  20. isar/mission_planner/local_planner.py +6 -20
  21. isar/mission_planner/mission_planner_interface.py +1 -1
  22. isar/models/events.py +184 -0
  23. isar/models/status.py +18 -0
  24. isar/modules.py +118 -199
  25. isar/robot/robot.py +377 -0
  26. isar/robot/robot_battery.py +60 -0
  27. isar/robot/robot_monitor_mission.py +357 -0
  28. isar/robot/robot_pause_mission.py +74 -0
  29. isar/robot/robot_resume_mission.py +67 -0
  30. isar/robot/robot_start_mission.py +66 -0
  31. isar/robot/robot_status.py +61 -0
  32. isar/robot/robot_stop_mission.py +68 -0
  33. isar/robot/robot_upload_inspection.py +75 -0
  34. isar/script.py +57 -40
  35. isar/services/service_connections/mqtt/mqtt_client.py +47 -11
  36. isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +5 -2
  37. isar/services/service_connections/mqtt/robot_info_publisher.py +3 -3
  38. isar/services/service_connections/persistent_memory.py +69 -0
  39. isar/services/utilities/mqtt_utilities.py +93 -0
  40. isar/services/utilities/robot_utilities.py +20 -0
  41. isar/services/utilities/scheduling_utilities.py +393 -65
  42. isar/state_machine/state_machine.py +219 -538
  43. isar/state_machine/states/__init__.py +0 -8
  44. isar/state_machine/states/await_next_mission.py +114 -0
  45. isar/state_machine/states/blocked_protective_stop.py +60 -0
  46. isar/state_machine/states/going_to_lockdown.py +95 -0
  47. isar/state_machine/states/going_to_recharging.py +92 -0
  48. isar/state_machine/states/home.py +115 -0
  49. isar/state_machine/states/intervention_needed.py +77 -0
  50. isar/state_machine/states/lockdown.py +38 -0
  51. isar/state_machine/states/maintenance.py +36 -0
  52. isar/state_machine/states/monitor.py +137 -247
  53. isar/state_machine/states/offline.py +51 -53
  54. isar/state_machine/states/paused.py +92 -23
  55. isar/state_machine/states/pausing.py +48 -0
  56. isar/state_machine/states/pausing_return_home.py +48 -0
  57. isar/state_machine/states/recharging.py +80 -0
  58. isar/state_machine/states/resuming.py +57 -0
  59. isar/state_machine/states/resuming_return_home.py +64 -0
  60. isar/state_machine/states/return_home_paused.py +109 -0
  61. isar/state_machine/states/returning_home.py +217 -0
  62. isar/state_machine/states/stopping.py +61 -0
  63. isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
  64. isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
  65. isar/state_machine/states/stopping_go_to_recharge.py +51 -0
  66. isar/state_machine/states/stopping_return_home.py +77 -0
  67. isar/state_machine/states/unknown_status.py +72 -0
  68. isar/state_machine/states_enum.py +21 -5
  69. isar/state_machine/transitions/mission.py +192 -0
  70. isar/state_machine/transitions/return_home.py +106 -0
  71. isar/state_machine/transitions/robot_status.py +80 -0
  72. isar/state_machine/utils/common_event_handlers.py +73 -0
  73. isar/storage/blob_storage.py +70 -52
  74. isar/storage/local_storage.py +25 -12
  75. isar/storage/storage_interface.py +28 -7
  76. isar/storage/uploader.py +174 -55
  77. isar/storage/utilities.py +32 -29
  78. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/METADATA +73 -110
  79. isar-1.34.9.dist-info/RECORD +135 -0
  80. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
  81. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/entry_points.txt +1 -0
  82. robot_interface/models/exceptions/robot_exceptions.py +91 -41
  83. robot_interface/models/initialize/__init__.py +0 -1
  84. robot_interface/models/inspection/__init__.py +0 -13
  85. robot_interface/models/inspection/inspection.py +42 -33
  86. robot_interface/models/mission/mission.py +14 -15
  87. robot_interface/models/mission/status.py +20 -26
  88. robot_interface/models/mission/task.py +154 -121
  89. robot_interface/models/robots/battery_state.py +6 -0
  90. robot_interface/models/robots/media.py +13 -0
  91. robot_interface/models/robots/robot_model.py +7 -7
  92. robot_interface/robot_interface.py +119 -84
  93. robot_interface/telemetry/mqtt_client.py +74 -12
  94. robot_interface/telemetry/payloads.py +91 -13
  95. robot_interface/utilities/json_service.py +7 -1
  96. isar/config/predefined_missions/default_turtlebot.json +0 -110
  97. isar/config/predefined_poses/__init__.py +0 -0
  98. isar/config/predefined_poses/predefined_poses.py +0 -616
  99. isar/config/settings.env +0 -25
  100. isar/mission_planner/sequential_task_selector.py +0 -23
  101. isar/mission_planner/task_selector_interface.py +0 -31
  102. isar/models/communication/__init__.py +0 -0
  103. isar/models/communication/message.py +0 -12
  104. isar/models/communication/queues/__init__.py +0 -4
  105. isar/models/communication/queues/queue_io.py +0 -12
  106. isar/models/communication/queues/queue_timeout_error.py +0 -2
  107. isar/models/communication/queues/queues.py +0 -19
  108. isar/models/communication/queues/status_queue.py +0 -20
  109. isar/models/mission_metadata/__init__.py +0 -0
  110. isar/services/readers/__init__.py +0 -0
  111. isar/services/readers/base_reader.py +0 -37
  112. isar/services/service_connections/stid/__init__.py +0 -0
  113. isar/services/utilities/queue_utilities.py +0 -39
  114. isar/state_machine/states/idle.py +0 -85
  115. isar/state_machine/states/initialize.py +0 -71
  116. isar/state_machine/states/initiate.py +0 -142
  117. isar/state_machine/states/off.py +0 -18
  118. isar/state_machine/states/stop.py +0 -95
  119. isar/storage/slimm_storage.py +0 -191
  120. isar-1.20.2.dist-info/RECORD +0 -116
  121. robot_interface/models/initialize/initialize_params.py +0 -9
  122. robot_interface/models/mission/step.py +0 -234
  123. {isar-1.20.2.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
  124. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,80 @@
1
+ from typing import TYPE_CHECKING, List
2
+
3
+ if TYPE_CHECKING:
4
+ from isar.state_machine.state_machine import StateMachine
5
+
6
+
7
+ def get_robot_status_transitions(state_machine: "StateMachine") -> List[dict]:
8
+ robot_status_transitions: List[dict] = [
9
+ {
10
+ "trigger": "initial_transition",
11
+ "source": state_machine.unknown_status_state,
12
+ "dest": state_machine.unknown_status_state,
13
+ },
14
+ {
15
+ "trigger": "initial_transition",
16
+ "source": state_machine.maintenance_state,
17
+ "dest": state_machine.maintenance_state,
18
+ },
19
+ {
20
+ "trigger": "robot_status_available",
21
+ "source": [
22
+ state_machine.unknown_status_state,
23
+ ],
24
+ "dest": state_machine.await_next_mission_state,
25
+ },
26
+ {
27
+ "trigger": "robot_status_available",
28
+ "source": [
29
+ state_machine.offline_state,
30
+ state_machine.blocked_protective_stopping_state,
31
+ state_machine.home_state,
32
+ ],
33
+ "dest": state_machine.intervention_needed_state,
34
+ },
35
+ {
36
+ "trigger": "robot_status_home",
37
+ "source": [
38
+ state_machine.home_state,
39
+ state_machine.blocked_protective_stopping_state,
40
+ state_machine.offline_state,
41
+ state_machine.unknown_status_state,
42
+ ],
43
+ "dest": state_machine.home_state,
44
+ },
45
+ {
46
+ "trigger": "robot_status_blocked_protective_stop",
47
+ "source": [
48
+ state_machine.home_state,
49
+ state_machine.offline_state,
50
+ state_machine.unknown_status_state,
51
+ ],
52
+ "dest": state_machine.blocked_protective_stopping_state,
53
+ },
54
+ {
55
+ "trigger": "robot_status_offline",
56
+ "source": [
57
+ state_machine.home_state,
58
+ state_machine.blocked_protective_stopping_state,
59
+ state_machine.unknown_status_state,
60
+ state_machine.recharging_state,
61
+ ],
62
+ "dest": state_machine.offline_state,
63
+ },
64
+ {
65
+ "trigger": "robot_status_unknown",
66
+ "source": [
67
+ state_machine.home_state,
68
+ state_machine.blocked_protective_stopping_state,
69
+ state_machine.offline_state,
70
+ state_machine.unknown_status_state,
71
+ ],
72
+ "dest": state_machine.unknown_status_state,
73
+ },
74
+ {
75
+ "trigger": "robot_recharged",
76
+ "source": state_machine.recharging_state,
77
+ "dest": state_machine.home_state,
78
+ },
79
+ ]
80
+ return robot_status_transitions
@@ -0,0 +1,73 @@
1
+ from typing import TYPE_CHECKING, Callable, Optional
2
+
3
+ from isar.apis.models.models import ControlMissionResponse, MissionStartResponse
4
+ from isar.models.events import Event
5
+ from robot_interface.models.mission.mission import Mission
6
+
7
+ if TYPE_CHECKING:
8
+ from isar.state_machine.state_machine import StateMachine
9
+
10
+
11
+ def start_mission_event_handler(
12
+ state_machine: "StateMachine",
13
+ event: Event[Mission],
14
+ response: Event[MissionStartResponse],
15
+ ) -> Optional[Callable]:
16
+ mission: Optional[Mission] = event.consume_event()
17
+ if not mission:
18
+ return None
19
+
20
+ if not state_machine.battery_level_is_above_mission_start_threshold():
21
+ response.trigger_event(
22
+ MissionStartResponse(
23
+ mission_id=mission.id,
24
+ mission_started=False,
25
+ mission_not_started_reason="Robot battery too low",
26
+ )
27
+ )
28
+ return None
29
+ state_machine.start_mission(mission=mission)
30
+ response.trigger_event(MissionStartResponse(mission_started=True))
31
+ return state_machine.start_mission_monitoring # type: ignore
32
+
33
+
34
+ def return_home_event_handler(
35
+ state_machine: "StateMachine", event: Event[bool]
36
+ ) -> Optional[Callable]:
37
+ if not event.consume_event():
38
+ return None
39
+
40
+ state_machine.events.api_requests.return_home.response.trigger_event(True)
41
+ state_machine.start_return_home_mission()
42
+ return state_machine.start_return_home_monitoring # type: ignore
43
+
44
+
45
+ def stop_mission_event_handler(
46
+ state_machine: "StateMachine", event: Event[str]
47
+ ) -> Optional[Callable]:
48
+ mission_id: str = event.consume_event()
49
+ if mission_id is None:
50
+ return None
51
+
52
+ if state_machine.shared_state.mission_id.check() == mission_id or mission_id == "":
53
+ state_machine.events.api_requests.stop_mission.response.trigger_event(
54
+ ControlMissionResponse(success=True)
55
+ )
56
+ state_machine.events.state_machine_events.stop_mission.trigger_event(True)
57
+ return state_machine.stop # type: ignore
58
+ else:
59
+ state_machine.events.api_requests.stop_mission.response.trigger_event(
60
+ ControlMissionResponse(success=False, failure_reason="Mission not found")
61
+ )
62
+ return None
63
+
64
+
65
+ def mission_started_event_handler(
66
+ state_machine: "StateMachine",
67
+ event: Event[bool],
68
+ ) -> Optional[Callable]:
69
+ if not event.consume_event():
70
+ return None
71
+
72
+ state_machine.logger.info("Received confirmation that mission has started")
73
+ return None
@@ -1,83 +1,101 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Union
4
3
 
5
4
  from azure.core.exceptions import ResourceExistsError
6
- from azure.storage.blob import BlobClient, BlobServiceClient, ContainerClient
7
- from injector import inject
5
+ from azure.storage.blob import BlobServiceClient, ContainerClient
8
6
 
9
7
  from isar.config.keyvault.keyvault_service import Keyvault
10
8
  from isar.config.settings import settings
11
- from isar.storage.storage_interface import StorageException, StorageInterface
9
+ from isar.storage.storage_interface import (
10
+ BlobStoragePath,
11
+ StorageException,
12
+ StorageInterface,
13
+ StoragePaths,
14
+ )
12
15
  from isar.storage.utilities import construct_metadata_file, construct_paths
13
- from robot_interface.models.inspection.inspection import Inspection
16
+ from robot_interface.models.inspection.inspection import InspectionBlob
14
17
  from robot_interface.models.mission.mission import Mission
15
18
 
16
19
 
17
20
  class BlobStorage(StorageInterface):
18
- @inject
19
- def __init__(
20
- self, keyvault: Keyvault, container_name: str = settings.BLOB_CONTAINER
21
- ):
22
- self.keyvault = keyvault
23
- self.storage_connection_string = self.keyvault.get_secret(
24
- "AZURE-STORAGE-CONNECTION-STRING"
25
- ).value
26
- self.container_name = container_name
27
-
28
- self.blob_service_client = self._get_blob_service_client()
29
- self.container_client = self._get_container_client(
30
- blob_service_client=self.blob_service_client
21
+ def __init__(self, keyvault: Keyvault) -> None:
22
+ self.logger = logging.getLogger("uploader")
23
+
24
+ self.container_client_data = self._get_container_client(
25
+ keyvault, "AZURE-STORAGE-CONNECTION-STRING-DATA"
26
+ )
27
+ self.container_client_metadata = self._get_container_client(
28
+ keyvault, "AZURE-STORAGE-CONNECTION-STRING-METADATA"
31
29
  )
32
30
 
33
- self.logger = logging.getLogger("uploader")
31
+ def _get_container_client(self, keyvault: Keyvault, secret_name: str):
32
+ storage_connection_string = keyvault.get_secret(secret_name).value
34
33
 
35
- def store(self, inspection: Inspection, mission: Mission) -> Union[str, dict]:
36
- data_path, metadata_path = construct_paths(
34
+ if storage_connection_string is None:
35
+ raise RuntimeError(f"{secret_name} from keyvault is None")
36
+
37
+ try:
38
+ blob_service_client = BlobServiceClient.from_connection_string(
39
+ storage_connection_string
40
+ )
41
+ except Exception as e:
42
+ self.logger.error("Unable to retrieve blob service client. Error: %s", e)
43
+ raise e
44
+
45
+ container_client = blob_service_client.get_container_client(
46
+ settings.BLOB_CONTAINER
47
+ )
48
+
49
+ if not container_client.exists():
50
+ raise RuntimeError(
51
+ "The configured blob container %s does not exist",
52
+ settings.BLOB_CONTAINER,
53
+ )
54
+ return container_client
55
+
56
+ def store(
57
+ self, inspection: InspectionBlob, mission: Mission
58
+ ) -> StoragePaths[BlobStoragePath]:
59
+ if inspection.data is None:
60
+ raise StorageException("Nothing to store. The inspection data is empty")
61
+
62
+ data_filename, metadata_filename = construct_paths(
37
63
  inspection=inspection, mission=mission
38
64
  )
39
65
 
40
66
  metadata_bytes: bytes = construct_metadata_file(
41
- inspection=inspection, mission=mission, filename=data_path.name
67
+ inspection=inspection, mission=mission, filename=data_filename.name
42
68
  )
43
69
 
44
- self._upload_file(path=metadata_path, data=metadata_bytes)
45
- return self._upload_file(path=data_path, data=inspection.data)
70
+ data_path = self._upload_file(
71
+ filename=data_filename,
72
+ data=inspection.data,
73
+ container_client=self.container_client_data,
74
+ )
75
+ metadata_path = self._upload_file(
76
+ filename=metadata_filename,
77
+ data=metadata_bytes,
78
+ container_client=self.container_client_metadata,
79
+ )
80
+ return StoragePaths(data_path=data_path, metadata_path=metadata_path)
46
81
 
47
- def _upload_file(self, path: Path, data: bytes) -> Union[str, dict]:
48
- blob_client = self._get_blob_client(path)
82
+ def _upload_file(
83
+ self, filename: Path, data: bytes, container_client: ContainerClient
84
+ ) -> BlobStoragePath:
85
+ blob_client = container_client.get_blob_client(filename.as_posix())
49
86
  try:
50
- blob_properties = blob_client.upload_blob(data=data)
87
+ blob_client.upload_blob(data=data)
51
88
  except ResourceExistsError as e:
52
89
  self.logger.error(
53
- f"Blob {path.as_posix()} already exists in container. Error: {e}"
90
+ "Blob %s already exists in container. Error: %s", filename.as_posix(), e
54
91
  )
55
92
  raise StorageException from e
56
93
  except Exception as e:
57
94
  self.logger.error("An unexpected error occurred while uploading blob")
58
95
  raise StorageException from e
59
96
 
60
- absolute_inspection_path = {
61
- "source": "blob",
62
- "blob_storage_account_url": settings.BLOB_STORAGE_ACCOUNT_URL,
63
- "blob_container": settings.BLOB_CONTAINER,
64
- "blob_name": blob_client.blob_name,
65
- }
66
- return absolute_inspection_path
67
-
68
- def _get_blob_service_client(self) -> BlobServiceClient:
69
- try:
70
- return BlobServiceClient.from_connection_string(
71
- self.storage_connection_string
72
- )
73
- except Exception as e:
74
- self.logger.error(f"Unable to retrieve blob service client. Error: {e}")
75
- raise e
76
-
77
- def _get_container_client(
78
- self, blob_service_client: BlobServiceClient
79
- ) -> ContainerClient:
80
- return blob_service_client.get_container_client(self.container_name)
81
-
82
- def _get_blob_client(self, path_to_blob: Path) -> BlobClient:
83
- return self.container_client.get_blob_client(path_to_blob.as_posix())
97
+ return BlobStoragePath(
98
+ storage_account=settings.BLOB_STORAGE_ACCOUNT,
99
+ blob_container=settings.BLOB_CONTAINER,
100
+ blob_name=blob_client.blob_name,
101
+ )
@@ -2,9 +2,14 @@ import logging
2
2
  from pathlib import Path
3
3
 
4
4
  from isar.config.settings import settings
5
- from isar.storage.storage_interface import StorageException, StorageInterface
5
+ from isar.storage.storage_interface import (
6
+ LocalStoragePath,
7
+ StorageException,
8
+ StorageInterface,
9
+ StoragePaths,
10
+ )
6
11
  from isar.storage.utilities import construct_metadata_file, construct_paths
7
- from robot_interface.models.inspection.inspection import Inspection
12
+ from robot_interface.models.inspection.inspection import InspectionBlob
8
13
  from robot_interface.models.mission.mission import Mission
9
14
 
10
15
 
@@ -13,30 +18,35 @@ class LocalStorage(StorageInterface):
13
18
  self.root_folder: Path = Path(settings.LOCAL_STORAGE_PATH)
14
19
  self.logger = logging.getLogger("uploader")
15
20
 
16
- def store(self, inspection: Inspection, mission: Mission) -> str:
17
- local_path, local_metadata_path = construct_paths(
21
+ def store(
22
+ self, inspection: InspectionBlob, mission: Mission
23
+ ) -> StoragePaths[LocalStoragePath]:
24
+ if inspection.data is None:
25
+ raise StorageException("Nothing to store. The inspection data is empty")
26
+
27
+ local_filename, local_metadata_filename = construct_paths(
18
28
  inspection=inspection, mission=mission
19
29
  )
20
30
 
21
- absolute_path: Path = self.root_folder.joinpath(local_path)
22
- absolute_metadata_path: Path = self.root_folder.joinpath(local_metadata_path)
31
+ data_path: Path = self.root_folder.joinpath(local_filename)
32
+ metadata_path: Path = self.root_folder.joinpath(local_metadata_filename)
23
33
 
24
- absolute_path.parent.mkdir(parents=True, exist_ok=True)
34
+ data_path.parent.mkdir(parents=True, exist_ok=True)
25
35
 
26
36
  metadata_bytes: bytes = construct_metadata_file(
27
- inspection=inspection, mission=mission, filename=local_path.name
37
+ inspection=inspection, mission=mission, filename=local_filename.name
28
38
  )
29
39
  try:
30
40
  with (
31
- open(absolute_path, "wb") as file,
32
- open(absolute_metadata_path, "wb") as metadata_file,
41
+ open(data_path, "wb") as file,
42
+ open(metadata_path, "wb") as metadata_file,
33
43
  ):
34
44
  file.write(inspection.data)
35
45
  metadata_file.write(metadata_bytes)
36
46
  except IOError as e:
37
47
  self.logger.warning(
38
48
  f"Failed open/write for one of the following files: \n"
39
- f"{absolute_path}\n{absolute_metadata_path}"
49
+ f"{data_path}\n{metadata_path}"
40
50
  )
41
51
  raise StorageException from e
42
52
  except Exception as e:
@@ -44,4 +54,7 @@ class LocalStorage(StorageInterface):
44
54
  "An unexpected error occurred while writing to local storage"
45
55
  )
46
56
  raise StorageException from e
47
- return str(absolute_path)
57
+ return StoragePaths(
58
+ data_path=LocalStoragePath(file_path=data_path),
59
+ metadata_path=LocalStoragePath(file_path=metadata_path),
60
+ )
@@ -1,25 +1,46 @@
1
1
  from abc import ABCMeta, abstractmethod
2
- from typing import Union
2
+ from pathlib import Path
3
+ from typing import Generic, TypeVar
3
4
 
4
- from robot_interface.models.inspection.inspection import Inspection
5
+ from pydantic import BaseModel
6
+
7
+ from robot_interface.models.inspection.inspection import InspectionBlob
5
8
  from robot_interface.models.mission.mission import Mission
6
9
 
7
10
 
11
+ class BlobStoragePath(BaseModel):
12
+ storage_account: str
13
+ blob_container: str
14
+ blob_name: str
15
+
16
+
17
+ class LocalStoragePath(BaseModel):
18
+ file_path: Path
19
+
20
+
21
+ TPath = TypeVar("TPath", BlobStoragePath, LocalStoragePath)
22
+
23
+
24
+ class StoragePaths(BaseModel, Generic[TPath]):
25
+ data_path: TPath
26
+ metadata_path: TPath
27
+
28
+
8
29
  class StorageInterface(metaclass=ABCMeta):
9
30
  @abstractmethod
10
- def store(self, inspection: Inspection, mission: Mission) -> Union[str, dict]:
31
+ def store(self, inspection: InspectionBlob, mission: Mission) -> StoragePaths:
11
32
  """
12
33
  Parameters
13
34
  ----------
35
+ inspection : InspectionBlob
36
+ The inspection object to be stored.
14
37
  mission : Mission
15
38
  Mission the inspection is a part of.
16
- inspection : Inspection
17
- The inspection object to be stored.
18
39
 
19
40
  Returns
20
41
  ----------
21
- String
22
- Path of the saved inspection
42
+ StoragePaths
43
+ Paths to the data and metadata
23
44
 
24
45
  Raises
25
46
  ----------