digitalkin 0.2.10__py3-none-any.whl → 0.2.12__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.
digitalkin/__version__.py CHANGED
@@ -5,4 +5,4 @@ from importlib.metadata import PackageNotFoundError, version
5
5
  try:
6
6
  __version__ = version("digitalkin")
7
7
  except PackageNotFoundError:
8
- __version__ = "0.2.10"
8
+ __version__ = "0.2.12"
@@ -96,6 +96,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer):
96
96
  input_data,
97
97
  setup_data,
98
98
  mission_id=request.mission_id,
99
+ setup_version_id=setup_data_class.current_setup_version.id,
99
100
  callback=self.add_to_queue,
100
101
  )
101
102
  job_id, module = result
@@ -48,17 +48,19 @@ class BaseModule(ABC, Generic[InputModelT, OutputModelT, SetupModelT, SecretMode
48
48
  def _init_strategies(self) -> None:
49
49
  """Initialize the services configuration."""
50
50
  for service_name in self.services_config.valid_strategy_names():
51
- service = self.services_config.init_strategy(service_name, self.mission_id)
51
+ service = self.services_config.init_strategy(service_name, self.mission_id, self.setup_version_id)
52
52
  setattr(self, service_name, service)
53
53
 
54
54
  def __init__(
55
55
  self,
56
56
  job_id: str,
57
57
  mission_id: str,
58
+ setup_version_id: str,
58
59
  ) -> None:
59
60
  """Initialize the module."""
60
61
  self.job_id: str = job_id
61
62
  self.mission_id: str = mission_id
63
+ self.setup_version_id: str = setup_version_id
62
64
  self._status = ModuleStatus.CREATED
63
65
  self._task: asyncio.Task | None = None
64
66
  # Initialize services configuration
@@ -79,6 +79,7 @@ class JobManager(ArgParser):
79
79
  input_data: InputModelT,
80
80
  setup_data: SetupModelT,
81
81
  mission_id: str,
82
+ setup_version_id: str,
82
83
  callback: Callable[[str, OutputModelT], Coroutine[Any, Any, None]],
83
84
  ) -> tuple[str, BaseModule[InputModelT, OutputModelT, SetupModelT, SecretModelT]]: # type: ignore
84
85
  """Start new module job in background (asyncio).
@@ -94,7 +95,7 @@ class JobManager(ArgParser):
94
95
  job_id = str(uuid.uuid4())
95
96
  """TODO: check uniqueness of the job_id"""
96
97
  # Création et démarrage du module
97
- module = self.module_class(job_id, mission_id=mission_id)
98
+ module = self.module_class(job_id, mission_id=mission_id, setup_version_id=setup_version_id)
98
99
  self.modules[job_id] = module
99
100
  try:
100
101
  await module.start(input_data, setup_data, await JobManager._job_specific_callback(callback, job_id))
@@ -9,10 +9,12 @@ class BaseStrategy(ABC):
9
9
  This class defines the interface for all strategies.
10
10
  """
11
11
 
12
- def __init__(self, mission_id: str) -> None:
12
+ def __init__(self, mission_id: str, setup_version_id: str) -> None:
13
13
  """Initialize the strategy.
14
14
 
15
15
  Args:
16
16
  mission_id: The ID of the mission this strategy is associated with
17
+ setup_version_id: The ID of the setup version this strategy is associated with
17
18
  """
18
19
  self.mission_id: str = mission_id
20
+ self.setup_version_id: str = setup_version_id
@@ -1,8 +1,8 @@
1
1
  """This module contains the abstract base class for cost strategies."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from enum import Enum, auto
5
- from typing import Any
4
+ from enum import Enum
5
+ from typing import Literal
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
@@ -10,36 +10,84 @@ from digitalkin.services.base_strategy import BaseStrategy
10
10
 
11
11
 
12
12
  class CostType(Enum):
13
- """."""
13
+ """Enum defining the types of costs that can be registered."""
14
14
 
15
- OTHER = auto()
16
- TOKEN_INPUT = auto()
17
- TOKEN_OUTPUT = auto()
18
- API_CALL = auto()
19
- STORAGE = auto()
20
- TIME = auto()
15
+ OTHER = "OTHER"
16
+ TOKEN_INPUT = "TOKEN_INPUT"
17
+ TOKEN_OUTPUT = "TOKEN_OUTPUT"
18
+ API_CALL = "API_CALL"
19
+ STORAGE = "STORAGE"
20
+ TIME = "TIME"
21
+
22
+
23
+ class CostConfig(BaseModel):
24
+ """Pydantic model that defines a cost configuration.
25
+
26
+ :param cost_name: Name of the cost (unique identifier in the service).
27
+ :param cost_type: The type/category of the cost.
28
+ :param description: A short description of the cost.
29
+ :param unit: The unit of measurement (e.g. token, call, MB).
30
+ :param rate: The cost per unit (e.g. dollars per token).
31
+ """
32
+
33
+ cost_name: str
34
+ cost_type: Literal["TOKEN_INPUT", "TOKEN_OUTPUT", "API_CALL", "STORAGE", "TIME", "OTHER"]
35
+ description: str | None = None
36
+ unit: str
37
+ rate: float
21
38
 
22
39
 
23
40
  class CostData(BaseModel):
24
- """."""
41
+ """Data model for cost operations."""
25
42
 
26
43
  cost: float
27
44
  mission_id: str
28
45
  name: str
29
- type: CostType
46
+ cost_type: CostType
30
47
  unit: str
48
+ rate: float
49
+ setup_version_id: str
50
+ quantity: float
51
+
52
+
53
+ class CostServiceError(Exception):
54
+ """Custom exception for CostService errors."""
31
55
 
32
56
 
33
57
  class CostStrategy(BaseStrategy, ABC):
34
58
  """Abstract base class for cost strategies."""
35
59
 
60
+ def __init__(self, mission_id: str, setup_version_id: str, config: dict[str, CostConfig]) -> None:
61
+ """Initialize the strategy.
62
+
63
+ Args:
64
+ mission_id: The ID of the mission this strategy is associated with
65
+ setup_version_id: The ID of the setup version this strategy is associated with
66
+ config: Configuration dictionary for the strategy
67
+ """
68
+ super().__init__(mission_id, setup_version_id)
69
+ self.config = config
70
+
36
71
  @abstractmethod
37
- def add_cost(self, cost_dict: dict[str, Any]) -> str:
72
+ def add(
73
+ self,
74
+ name: str,
75
+ cost_config_name: str,
76
+ quantity: float,
77
+ ) -> None:
38
78
  """Register a new cost."""
39
79
 
40
- def __post_init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
41
- """Allow post init configuration."""
42
-
43
80
  @abstractmethod
44
- def get(self, cost_dict: dict[str, Any]) -> list[CostData]:
81
+ def get(
82
+ self,
83
+ name: str,
84
+ ) -> list[CostData]:
45
85
  """Get a cost."""
86
+
87
+ @abstractmethod
88
+ def get_filtered(
89
+ self,
90
+ names: list[str] | None = None,
91
+ cost_types: list[Literal["TOKEN_INPUT", "TOKEN_OUTPUT", "API_CALL", "STORAGE", "TIME", "OTHER"]] | None = None,
92
+ ) -> list[CostData]:
93
+ """Get filtered costs."""
@@ -1,9 +1,9 @@
1
1
  """Default cost."""
2
2
 
3
3
  import logging
4
- from typing import Any
4
+ from typing import Literal
5
5
 
6
- from digitalkin.services.cost.cost_strategy import CostData, CostStrategy
6
+ from digitalkin.services.cost.cost_strategy import CostConfig, CostData, CostServiceError, CostStrategy, CostType
7
7
 
8
8
  logger = logging.getLogger(__name__)
9
9
 
@@ -11,20 +11,99 @@ logger = logging.getLogger(__name__)
11
11
  class DefaultCost(CostStrategy):
12
12
  """Default cost strategy."""
13
13
 
14
- def add_cost(self, cost_dict: dict[str, Any]) -> str: # noqa: PLR6301
14
+ def __init__(self, mission_id: str, setup_version_id: str, config: dict[str, CostConfig]) -> None:
15
+ """Initialize the strategy.
16
+
17
+ Args:
18
+ mission_id: The ID of the mission this strategy is associated with
19
+ setup_version_id: The ID of the setup version this strategy is associated with
20
+ config: The configuration dictionary for the cost
21
+ """
22
+ super().__init__(mission_id=mission_id, setup_version_id=setup_version_id, config=config)
23
+ self.db: dict[str, list[CostData]] = {}
24
+
25
+ def add(
26
+ self,
27
+ name: str,
28
+ cost_config_name: str,
29
+ quantity: float,
30
+ ) -> None:
15
31
  """Create a new record in the cost database.
16
32
 
33
+ Args:
34
+ name: The name of the cost
35
+ cost_config_name: The name of the cost config
36
+ quantity: The quantity of the cost
37
+
38
+ Raises:
39
+ CostServiceError: If the cost data is invalid or if the cost already exists
40
+ """
41
+ cost_config = self.config.get(cost_config_name)
42
+ if cost_config is None:
43
+ msg = f"Cost config {cost_config_name} not found in the configuration."
44
+ logger.error(msg)
45
+ raise CostServiceError(msg)
46
+ cost_data = CostData.model_validate({
47
+ "name": name,
48
+ "cost": cost_config.rate * quantity,
49
+ "unit": cost_config.unit,
50
+ "cost_type": getattr(CostType, cost_config.cost_type),
51
+ "mission_id": self.mission_id,
52
+ "rate": cost_config.rate,
53
+ "quantity": quantity,
54
+ "setup_version_id": self.setup_version_id,
55
+ })
56
+ if cost_data.mission_id not in self.db:
57
+ self.db[cost_data.mission_id] = []
58
+ if cost_data.name in [cost.name for cost in self.db[cost_data.mission_id]]:
59
+ msg = f"Cost with name {cost_data.name} already exists in mission {cost_data.mission_id}"
60
+ logger.error(msg)
61
+ raise CostServiceError(msg)
62
+ self.db[cost_data.mission_id].append(cost_data)
63
+
64
+ def get(self, name: str) -> list[CostData]:
65
+ """Get a record from the database.
66
+
67
+ Args:
68
+ name: The name of the cost
69
+
17
70
  Returns:
18
- str: The ID of the new record
71
+ list[CostData]: The cost data
72
+
73
+ Raises:
74
+ CostServiceError: If the cost data is invalid or if the cost does not exist
19
75
  """
20
- logger.info("Cost added with cost_dict: %s", cost_dict)
21
- return ""
76
+ if self.mission_id not in self.db:
77
+ msg = f"Mission {self.mission_id} not found in the database."
78
+ logger.warning(msg)
79
+ raise CostServiceError(msg)
80
+
81
+ return [cost for cost in self.db[self.mission_id] if cost.name == name] or []
22
82
 
23
- def get(self, cost_dict: dict[str, Any]) -> list[CostData]: # noqa: PLR6301
83
+ def get_filtered(
84
+ self,
85
+ names: list[str] | None = None,
86
+ cost_types: list[Literal["TOKEN_INPUT", "TOKEN_OUTPUT", "API_CALL", "STORAGE", "TIME", "OTHER"]] | None = None,
87
+ ) -> list[CostData]:
24
88
  """Get records from the database.
25
89
 
90
+ Args:
91
+ names: The names of the costs
92
+ cost_types: The types of the costs
93
+
26
94
  Returns:
27
95
  list[CostData]: The list of records
96
+
97
+ Raises:
98
+ CostServiceError: If the cost data is invalid or if the cost does not exist
28
99
  """
29
- logger.info("Costs querried with cost_dict: %s", cost_dict)
30
- return []
100
+ if self.mission_id not in self.db:
101
+ msg = f"Mission {self.mission_id} not found in the database."
102
+ logger.warning(msg)
103
+ raise CostServiceError(msg)
104
+
105
+ return [
106
+ cost
107
+ for cost in self.db[self.mission_id]
108
+ if (names and cost.name in names) or (cost_types and cost.cost_type in cost_types)
109
+ ]
@@ -1,15 +1,17 @@
1
1
  """This module implements the default Cost strategy."""
2
2
 
3
3
  import logging
4
- from typing import Any
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ from typing import Any, Literal
5
7
 
6
8
  from digitalkin_proto.digitalkin.cost.v1 import cost_pb2, cost_service_pb2_grpc
7
9
  from google.protobuf import json_format
8
- from pydantic import ValidationError
9
10
 
11
+ from digitalkin.grpc_servers.utils.exceptions import ServerError
10
12
  from digitalkin.grpc_servers.utils.grpc_client_wrapper import GrpcClientWrapper
11
- from digitalkin.grpc_servers.utils.models import ServerConfig
12
- from digitalkin.services.cost.cost_strategy import CostData, CostStrategy
13
+ from digitalkin.grpc_servers.utils.models import ClientConfig
14
+ from digitalkin.services.cost.cost_strategy import CostConfig, CostData, CostServiceError, CostStrategy, CostType
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
@@ -17,65 +19,144 @@ logger = logging.getLogger(__name__)
17
19
  class GrpcCost(CostStrategy, GrpcClientWrapper):
18
20
  """This class implements the default Cost strategy."""
19
21
 
20
- def _get_costs_by_name(self, cost_dict: dict[str, Any]) -> list[CostData]:
21
- request = cost_pb2.GetCostsByNameRequest(
22
- mission_id=cost_dict["mission_id"],
23
- name=cost_dict["name"],
24
- )
25
- response = self.exec_grpc_query("GetCostsByName", request)
26
- return [CostData(**json_format.MessageToDict(cost)) for cost in response.costs]
27
-
28
- def _get_costs_by_mission(self, cost_dict: dict[str, Any]) -> list[CostData]:
29
- request = cost_pb2.GetCostsByMissionRequest(mission_id=cost_dict["mission_id"])
30
- response = self.exec_grpc_query("GetCostsByMission", request)
31
- return [CostData(**json_format.MessageToDict(cost)) for cost in response.costs]
32
-
33
- def _get_costs_by_type(self, cost_dict: dict[str, Any]) -> list[CostData]:
34
- request = cost_pb2.GetCostsByTypeRequest(
35
- mission_id=cost_dict["mission_id"],
36
- type=cost_dict["type"],
37
- )
38
- response = self.exec_grpc_query("GetCostsBytype", request)
39
- return [CostData(**json_format.MessageToDict(cost)) for cost in response.costs]
40
-
41
- def __init__(self, mission_id: str, config: ServerConfig) -> None:
22
+ @staticmethod
23
+ @contextmanager
24
+ def _handle_grpc_errors(operation: str) -> Generator[Any, Any, Any]:
25
+ """Context manager for consistent gRPC error handling.
26
+
27
+ Yields:
28
+ Allow error handling in context.
29
+
30
+ Args:
31
+ operation: Description of the operation being performed.
32
+
33
+ Raises:
34
+ ValueError: Error with the model validation.
35
+ ServerError: from gRPC Client.
36
+ CostServiceError: Unexpected error.
37
+ """
38
+ try:
39
+ yield
40
+ except CostServiceError as e:
41
+ msg = f"CostServiceError in {operation}: {e}"
42
+ logger.exception(msg)
43
+ raise CostServiceError(msg) from e
44
+ except ServerError as e:
45
+ msg = f"gRPC {operation} failed: {e}"
46
+ logger.exception(msg)
47
+ raise ServerError(msg) from e
48
+ except Exception as e:
49
+ msg = f"Unexpected error in {operation}"
50
+ logger.exception(msg)
51
+ raise CostServiceError(msg) from e
52
+
53
+ def __init__(
54
+ self, mission_id: str, setup_version_id: str, config: dict[str, CostConfig], client_config: ClientConfig
55
+ ) -> None:
42
56
  """Initialize the cost."""
43
- super().__init__(mission_id)
44
- channel = self._init_channel(config)
57
+ super().__init__(mission_id=mission_id, setup_version_id=setup_version_id, config=config)
58
+ channel = self._init_channel(client_config)
45
59
  self.stub = cost_service_pb2_grpc.CostServiceStub(channel)
46
60
  logger.info("Channel client 'Cost' initialized succesfully")
47
61
 
48
- def add_cost(self, cost_dict: dict[str, Any]) -> str:
62
+ def add(
63
+ self,
64
+ name: str,
65
+ cost_config_name: str,
66
+ quantity: float,
67
+ ) -> None:
49
68
  """Create a new record in the cost database.
50
69
 
51
- Required arguments:
52
- data: Object representation of CostData
70
+ Args:
71
+ name: The name of the cost
72
+ cost_config_name: The name of the cost config
73
+ quantity: The quantity of the cost
53
74
 
54
- Returns:
55
- str: The ID of the new record
75
+ Raises:
76
+ CostServiceError: If the cost config is invalid
56
77
  """
57
- try:
58
- valid_data = CostData.model_validate(cost_dict["data"])
59
- except ValidationError:
60
- logger.exception("Validation failed for model StorageData")
61
- return ""
62
- except KeyError:
63
- logger.exception("Missing mandatory 'data' in dict.")
64
- return ""
65
-
66
- request = cost_pb2.AddCostRequest(**valid_data.model_dump())
67
- return self.exec_grpc_query("AddCost", request)
78
+ with self._handle_grpc_errors("AddCost"):
79
+ cost_config = self.config.get(cost_config_name)
80
+ if cost_config is None:
81
+ msg = f"Cost config {cost_config_name} not found in the configuration."
82
+ logger.error(msg)
83
+ raise CostServiceError(msg)
84
+ valid_data = CostData.model_validate({
85
+ "name": name,
86
+ "cost": cost_config.rate * quantity,
87
+ "unit": cost_config.unit,
88
+ "cost_type": CostType[cost_config.cost_type],
89
+ "mission_id": self.mission_id,
90
+ "rate": cost_config.rate,
91
+ "quantity": quantity,
92
+ "setup_version_id": self.setup_version_id,
93
+ })
94
+ request = cost_pb2.AddCostRequest(
95
+ cost=valid_data.cost,
96
+ name=valid_data.name,
97
+ unit=valid_data.unit,
98
+ cost_type=valid_data.cost_type.name,
99
+ mission_id=valid_data.mission_id,
100
+ rate=valid_data.rate,
101
+ quantity=valid_data.quantity,
102
+ setup_version_id=valid_data.setup_version_id,
103
+ )
104
+ self.exec_grpc_query("AddCost", request)
105
+ logger.debug("Cost added with cost_dict: %s", valid_data.model_dump())
106
+
107
+ def get(self, name: str) -> list[CostData]:
108
+ """Get a record from the database.
109
+
110
+ Args:
111
+ name: The name of the cost
68
112
 
69
- def get(self, cost_dict: dict[str, Any]) -> list[CostData]:
70
- """Get records from the database.
113
+ Returns:
114
+ CostData: The cost data
115
+ """
116
+ with self._handle_grpc_errors("GetCost"):
117
+ request = cost_pb2.GetCostRequest(name=name, mission_id=self.mission_id)
118
+ response: cost_pb2.GetCostResponse = self.exec_grpc_query("GetCost", request)
119
+ cost_data_list = [
120
+ json_format.MessageToDict(
121
+ cost,
122
+ preserving_proto_field_name=True,
123
+ always_print_fields_with_no_presence=True,
124
+ )
125
+ for cost in response.costs
126
+ ]
127
+ logger.debug("Costs retrieved with cost_dict: %s", cost_data_list)
128
+ return [CostData.model_validate(cost_data) for cost_data in cost_data_list]
129
+
130
+ def get_filtered(
131
+ self,
132
+ names: list[str] | None = None,
133
+ cost_types: list[Literal["TOKEN_INPUT", "TOKEN_OUTPUT", "API_CALL", "STORAGE", "TIME", "OTHER"]] | None = None,
134
+ ) -> list[CostData]:
135
+ """Get a list of records from the database.
136
+
137
+ Args:
138
+ names: The names of the costs
139
+ cost_types: The types of the costs
71
140
 
72
141
  Returns:
73
- list[CostData]: The list of records
142
+ list[CostData]: The cost data
74
143
  """
75
- if "mission_id" not in cost_dict:
76
- return []
77
- if "name" in cost_dict:
78
- return self._get_costs_by_name(cost_dict)
79
- if "type" in cost_dict:
80
- return self._get_costs_by_type(cost_dict)
81
- return self._get_costs_by_mission(cost_dict)
144
+ with self._handle_grpc_errors("GetCosts"):
145
+ request = cost_pb2.GetCostsRequest(
146
+ mission_id=self.mission_id,
147
+ filter=cost_pb2.CostFilter(
148
+ names=names or [],
149
+ cost_types=cost_types or [],
150
+ ),
151
+ )
152
+ response: cost_pb2.GetCostsResponse = self.exec_grpc_query("GetCosts", request)
153
+ cost_data_list = [
154
+ json_format.MessageToDict(
155
+ cost,
156
+ preserving_proto_field_name=True,
157
+ always_print_fields_with_no_presence=True,
158
+ )
159
+ for cost in response.costs
160
+ ]
161
+ logger.debug("Filtered costs retrieved with cost_dict: %s", cost_data_list)
162
+ return [CostData.model_validate(cost_data) for cost_data in cost_data_list]
@@ -18,14 +18,15 @@ logger = logging.getLogger(__name__)
18
18
  class DefaultFilesystem(FilesystemStrategy):
19
19
  """Default state filesystem strategy."""
20
20
 
21
- def __init__(self, mission_id: str, config: dict[str, str]) -> None:
21
+ def __init__(self, mission_id: str, setup_version_id: str, config: dict[str, str]) -> None:
22
22
  """Initialize the default filesystem strategy.
23
23
 
24
24
  Args:
25
25
  mission_id: The ID of the mission this strategy is associated with
26
+ setup_version_id: The ID of the setup version this strategy is associated with
26
27
  config: A dictionary mapping names to Pydantic model classes
27
28
  """
28
- super().__init__(mission_id, config)
29
+ super().__init__(mission_id, setup_version_id, config)
29
30
  self.temp_root: str = self.config.get("temp_root", "") or tempfile.gettempdir()
30
31
  os.makedirs(self.temp_root, exist_ok=True)
31
32
  self.db: dict[str, FilesystemData] = {}
@@ -35,14 +35,15 @@ class FilesystemData(BaseModel):
35
35
  class FilesystemStrategy(BaseStrategy, ABC):
36
36
  """Abstract base class for filesystem strategies."""
37
37
 
38
- def __init__(self, mission_id: str, config: dict[str, str]) -> None:
38
+ def __init__(self, mission_id: str, setup_version_id: str, config: dict[str, str]) -> None:
39
39
  """Initialize the strategy.
40
40
 
41
41
  Args:
42
42
  mission_id: The ID of the mission this strategy is associated with
43
+ setup_version_id: The ID of the setup version this strategy is associated with
43
44
  config: configuration dictionary for the filesystem strategy
44
45
  """
45
- super().__init__(mission_id)
46
+ super().__init__(mission_id, setup_version_id)
46
47
  self.config: dict[str, str] = config
47
48
 
48
49
  @abstractmethod
@@ -24,26 +24,6 @@ logger = logging.getLogger(__name__)
24
24
  class GrpcFilesystem(FilesystemStrategy, GrpcClientWrapper):
25
25
  """Default state filesystem strategy."""
26
26
 
27
- def __init__(
28
- self,
29
- mission_id: str,
30
- config: dict[str, str],
31
- client_config: ClientConfig,
32
- **kwargs, # noqa: ANN003, ARG002
33
- ) -> None:
34
- """Initialize the default filesystem strategy.
35
-
36
- Args:
37
- mission_id: The ID of the mission this strategy is associated with
38
- config: A dictionary mapping names to Pydantic model classes
39
- client_config: The client configuration object
40
- kwargs: other optional arguments to pass to the parent class constructor
41
- """
42
- super().__init__(mission_id, config)
43
- channel = self._init_channel(client_config)
44
- self.stub = filesystem_service_pb2_grpc.FilesystemServiceStub(channel)
45
- logger.info("Channel client 'Filesystem' initialized succesfully")
46
-
47
27
  @staticmethod
48
28
  @contextmanager
49
29
  def _handle_grpc_errors(operation: str) -> Generator[Any, Any, Any]:
@@ -56,7 +36,7 @@ class GrpcFilesystem(FilesystemStrategy, GrpcClientWrapper):
56
36
  operation: Description of the operation being performed.
57
37
 
58
38
  Raises:
59
- ValueError: Error wiht the model validation.
39
+ ValueError: Error with the model validation.
60
40
  ServerError: from gRPC Client.
61
41
  FilesystemServiceError: Filesystem service internal.
62
42
  """
@@ -71,6 +51,28 @@ class GrpcFilesystem(FilesystemStrategy, GrpcClientWrapper):
71
51
  logger.exception(msg)
72
52
  raise FilesystemServiceError(msg) from e
73
53
 
54
+ def __init__(
55
+ self,
56
+ mission_id: str,
57
+ setup_version_id: str,
58
+ config: dict[str, str],
59
+ client_config: ClientConfig,
60
+ **kwargs, # noqa: ANN003, ARG002
61
+ ) -> None:
62
+ """Initialize the default filesystem strategy.
63
+
64
+ Args:
65
+ mission_id: The ID of the mission this strategy is associated with
66
+ setup_version_id: The ID of the setup version this strategy is associated with
67
+ config: A dictionary mapping names to Pydantic model classes
68
+ client_config: The server configuration object
69
+ kwargs: other optional arguments to pass to the parent class constructor
70
+ """
71
+ super().__init__(mission_id, setup_version_id, config)
72
+ channel = self._init_channel(client_config)
73
+ self.stub = filesystem_service_pb2_grpc.FilesystemServiceStub(channel)
74
+ logger.info("Channel client 'Filesystem' initialized succesfully")
75
+
74
76
  def upload(self, content: bytes, name: str, file_type: FileType) -> FilesystemData:
75
77
  """Create a new file in the file system.
76
78
 
@@ -5,7 +5,7 @@ from typing import Any, ClassVar
5
5
  from pydantic import BaseModel, Field, PrivateAttr
6
6
 
7
7
  from digitalkin.services.agent import AgentStrategy, DefaultAgent
8
- from digitalkin.services.cost import CostStrategy, DefaultCost
8
+ from digitalkin.services.cost import CostStrategy, DefaultCost, GrpcCost
9
9
  from digitalkin.services.filesystem import DefaultFilesystem, FilesystemStrategy, GrpcFilesystem
10
10
  from digitalkin.services.identity import DefaultIdentity, IdentityStrategy
11
11
  from digitalkin.services.registry import DefaultRegistry, RegistryStrategy
@@ -30,7 +30,7 @@ class ServicesConfig(BaseModel):
30
30
  )
31
31
  _config_storage: dict[str, Any | None] = PrivateAttr(default_factory=dict)
32
32
  _cost: ServicesStrategy[CostStrategy] = PrivateAttr(
33
- default_factory=lambda: ServicesStrategy(local=DefaultCost, remote=DefaultCost)
33
+ default_factory=lambda: ServicesStrategy(local=DefaultCost, remote=GrpcCost)
34
34
  )
35
35
  _config_cost: dict[str, Any | None] = PrivateAttr(default_factory=dict)
36
36
  _snapshot: ServicesStrategy[SnapshotStrategy] = PrivateAttr(
@@ -111,12 +111,13 @@ class ServicesConfig(BaseModel):
111
111
  """
112
112
  return getattr(self, f"_config_{name}", {})
113
113
 
114
- def init_strategy(self, name: str, mission_id: str) -> ServicesStrategy:
114
+ def init_strategy(self, name: str, mission_id: str, setup_version_id: str) -> ServicesStrategy:
115
115
  """Initialize a specific strategy.
116
116
 
117
117
  Args:
118
118
  name: The name of the strategy to initialize
119
119
  mission_id: The ID of the mission this strategy is associated with
120
+ setup_version_id: The setup version ID for the strategy
120
121
 
121
122
  Returns:
122
123
  The initialized strategy instance
@@ -129,8 +130,8 @@ class ServicesConfig(BaseModel):
129
130
  msg = f"Strategy {name} not found in ServicesConfig."
130
131
  raise ValueError(msg)
131
132
 
132
- # Instantiate the strategy with the mission ID and configuration
133
- return strategy_type(mission_id, **self.get_strategy_config(name) or {})
133
+ # Instantiate the strategy with the mission ID, setup version ID, and configuration
134
+ return strategy_type(mission_id, setup_version_id, **self.get_strategy_config(name) or {})
134
135
 
135
136
  @property
136
137
  def storage(self) -> type[StorageStrategy]:
@@ -209,12 +209,13 @@ class DefaultStorage(StorageStrategy):
209
209
  def __init__(
210
210
  self,
211
211
  mission_id: str,
212
+ setup_version_id: str,
212
213
  config: dict[str, type[BaseModel]],
213
214
  storage_file_path: str = "local_storage",
214
215
  **kwargs, # noqa: ANN003, ARG002
215
216
  ) -> None:
216
217
  """Initialize the storage."""
217
- super().__init__(mission_id=mission_id, config=config)
218
+ super().__init__(mission_id=mission_id, setup_version_id=setup_version_id, config=config)
218
219
  self.storage_file_path = f"{self.mission_id}_{storage_file_path}.json"
219
220
  self.storage_file = Path(self.storage_file_path)
220
221
  self.storage = self._load_from_file()
@@ -37,7 +37,7 @@ class GrpcStorage(StorageStrategy, GrpcClientWrapper):
37
37
  dtype = DataType[raw["data_type"]]
38
38
  payload = raw.get("data", {})
39
39
 
40
- validated = self._validate_data(rid, payload)
40
+ validated = self._validate_data(coll, payload)
41
41
  return StorageRecord(
42
42
  mission_id=mission,
43
43
  collection=coll,
@@ -185,12 +185,13 @@ class GrpcStorage(StorageStrategy, GrpcClientWrapper):
185
185
  def __init__(
186
186
  self,
187
187
  mission_id: str,
188
+ setup_version_id: str,
188
189
  config: dict[str, type[BaseModel]],
189
190
  client_config: ClientConfig,
190
191
  **kwargs, # noqa: ANN003, ARG002
191
192
  ) -> None:
192
193
  """Initialize the storage."""
193
- super().__init__(mission_id=mission_id, config=config)
194
+ super().__init__(mission_id=mission_id, setup_version_id=setup_version_id, config=config)
194
195
 
195
196
  channel = self._init_channel(client_config)
196
197
  self.stub = storage_service_pb2_grpc.StorageServiceStub(channel)
@@ -39,11 +39,11 @@ class StorageRecord(BaseModel):
39
39
  class StorageStrategy(BaseStrategy, ABC):
40
40
  """Define CRUD + list/remove-collection against a collection/record store."""
41
41
 
42
- def _validate_data(self, record_id: str, data: dict[str, Any]) -> BaseModel:
42
+ def _validate_data(self, collection: str, data: dict[str, Any]) -> BaseModel:
43
43
  """Validate data against the model schema for the given key.
44
44
 
45
45
  Args:
46
- record_id: The unique ID for the record
46
+ collection: The unique name for the record type
47
47
  data: The data to validate
48
48
 
49
49
  Returns:
@@ -52,15 +52,15 @@ class StorageStrategy(BaseStrategy, ABC):
52
52
  Raises:
53
53
  ValueError: If the key has no associated model or validation fails
54
54
  """
55
- model_cls = self.config.get(record_id)
55
+ model_cls = self.config.get(collection)
56
56
  if not model_cls:
57
- msg = f"No schema registered for collection '{record_id}'"
57
+ msg = f"No schema registered for collection '{collection}'"
58
58
  raise ValueError(msg)
59
59
 
60
60
  try:
61
61
  return model_cls.model_validate(data)
62
62
  except Exception as e:
63
- msg = f"Validation failed for '{record_id}': {e!s}"
63
+ msg = f"Validation failed for '{collection}': {e!s}"
64
64
  raise ValueError(msg) from e
65
65
 
66
66
  def _create_storage_record(
@@ -163,14 +163,15 @@ class StorageStrategy(BaseStrategy, ABC):
163
163
  True if the deletion was successful, False otherwise
164
164
  """
165
165
 
166
- def __init__(self, mission_id: str, config: dict[str, type[BaseModel]]) -> None:
166
+ def __init__(self, mission_id: str, setup_version_id: str, config: dict[str, type[BaseModel]]) -> None:
167
167
  """Initialize the storage strategy.
168
168
 
169
169
  Args:
170
170
  mission_id: The ID of the mission this strategy is associated with
171
+ setup_version_id: The ID of the setup version
171
172
  config: A dictionary mapping names to Pydantic model classes
172
173
  """
173
- super().__init__(mission_id)
174
+ super().__init__(mission_id, setup_version_id)
174
175
  # Schema configuration mapping keys to model classes
175
176
  self.config: dict[str, type[BaseModel]] = config
176
177
 
@@ -200,7 +201,7 @@ class StorageStrategy(BaseStrategy, ABC):
200
201
  raise ValueError(msg)
201
202
  record_id = record_id or uuid4().hex
202
203
  data_type_enum = DataType[data_type]
203
- validated_data = self._validate_data(record_id, {**data, "mission_id": self.mission_id})
204
+ validated_data = self._validate_data(collection, {**data, "mission_id": self.mission_id})
204
205
  record = self._create_storage_record(collection, record_id, validated_data, data_type_enum)
205
206
  return self._store(record)
206
207
 
@@ -227,7 +228,7 @@ class StorageStrategy(BaseStrategy, ABC):
227
228
  Returns:
228
229
  StorageRecord: The modified record
229
230
  """
230
- validated_data = self._validate_data(record_id, data)
231
+ validated_data = self._validate_data(collection, data)
231
232
  return self._update(collection, record_id, validated_data)
232
233
 
233
234
  def remove(self, collection: str, record_id: str) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.2.10
3
+ Version: 0.2.12
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -452,7 +452,7 @@ Classifier: License :: Other/Proprietary License
452
452
  Requires-Python: >=3.10
453
453
  Description-Content-Type: text/markdown
454
454
  License-File: LICENSE
455
- Requires-Dist: digitalkin-proto>=0.1.8
455
+ Requires-Dist: digitalkin-proto>=0.1.10
456
456
  Requires-Dist: grpcio-health-checking>=1.71.0
457
457
  Requires-Dist: grpcio-reflection>=1.71.0
458
458
  Requires-Dist: grpcio-status>=1.71.0
@@ -7,13 +7,13 @@ base_server/mock/__init__.py,sha256=YZFT-F1l_TpvJYuIPX-7kTeE1CfOjhx9YmNRXVoi-jQ,
7
7
  base_server/mock/mock_pb2.py,sha256=sETakcS3PAAm4E-hTCV1jIVaQTPEAIoVVHupB8Z_k7Y,1843
8
8
  base_server/mock/mock_pb2_grpc.py,sha256=BbOT70H6q3laKgkHfOx1QdfmCS_HxCY4wCOX84YAdG4,3180
9
9
  digitalkin/__init__.py,sha256=7LLBAba0th-3SGqcpqFO-lopWdUkVLKzLZiMtB-mW3M,162
10
- digitalkin/__version__.py,sha256=_lmtRGNN0s_rEpZfYljDoaivZSEvHnwiOHxMyt0abjo,191
10
+ digitalkin/__version__.py,sha256=NhqSkQ1MfEwCjG1RWIYDONpYEZDeyyeAwIaGaE1q-Tk,191
11
11
  digitalkin/logger.py,sha256=9cDgyJV2QXXT8F--xRODFlZyDgjuTTXNdpCU3GdqCsk,382
12
12
  digitalkin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  digitalkin/grpc_servers/__init__.py,sha256=0cJBlwipSmFdXkyH3T0i6OJ1WpAtNsZgYX7JaSnkbtg,804
14
14
  digitalkin/grpc_servers/_base_server.py,sha256=ec4xmgAuOMVg45a63O_PEa2T7mI4tJ6boxcXauFyZ5g,18649
15
15
  digitalkin/grpc_servers/module_server.py,sha256=0jC5MN-H4MGA9O9J87gnlM8EHcbrkb0chuCm8yZh7gI,10232
16
- digitalkin/grpc_servers/module_servicer.py,sha256=gm1dhmHhBGqY2xlhAp5i15O6waOA1mDHbFJxsT2RUT8,13662
16
+ digitalkin/grpc_servers/module_servicer.py,sha256=mbXbfCXjRZHl8ueaYIedWMqw0OneoBU60mtbHV5yAe4,13734
17
17
  digitalkin/grpc_servers/registry_server.py,sha256=PmWaH4Xmg5Sj7NtFVLBNTOzkOfqo7dw_qyVBnaW4jy4,2238
18
18
  digitalkin/grpc_servers/registry_servicer.py,sha256=mCAjNhdMq5DozZMEPsJK__DIxePEYxSWV-gAq-Xctk4,16469
19
19
  digitalkin/grpc_servers/utils/exceptions.py,sha256=I00OM8p8up20He4dU1fiHsvdLj1DymjR_UmoeUm2MSA,785
@@ -29,26 +29,26 @@ digitalkin/models/services/__init__.py,sha256=HsW7MUGFPvH7Ri28WN4BHHBfEQk5dzU_9F
29
29
  digitalkin/models/services/cost.py,sha256=QTEuFD6xz62nob0z4ksE-INJWcZ-iFiuNW5mvXhpFes,1599
30
30
  digitalkin/models/services/storage.py,sha256=cYTVIriGKiprF9OerhSxmc_jM6fUTVwmeon1yQCinkE,143
31
31
  digitalkin/modules/__init__.py,sha256=ppYARmhvdVi55ofC0QZerIempSlcJYDeCXhcl4qXObw,278
32
- digitalkin/modules/_base_module.py,sha256=WU2oUW4-FjFtieuyRfXv3R3P4N1xT78zzMWrSTdhmAg,9090
32
+ digitalkin/modules/_base_module.py,sha256=jzsB5d51Sp6-K_e9PxJBVLAv0TeVRo7WhYOKM2SvZ_4,9198
33
33
  digitalkin/modules/archetype_module.py,sha256=T2Ehj7EpAC2MO9WQbJv39hqRw7rh3exhVZTEL3JPM8U,421
34
- digitalkin/modules/job_manager.py,sha256=QHcrm3F99I9tlNzXefkyhkkwPITf_H1bEb_obMDbUic,6177
34
+ digitalkin/modules/job_manager.py,sha256=EB53_HZJZOkOR3MXidN8ZiJte8_kOJZ3hnOFVfm5v64,6243
35
35
  digitalkin/modules/tool_module.py,sha256=86g0M1wHZ1ReIc7AkKfyjnlGN2QYJBGxrEQpKVlyrZI,421
36
36
  digitalkin/modules/trigger_module.py,sha256=kVoI4Gdkw7WWUP5T6hSCNqw5FxibTxL6Tpq9KP7gg78,379
37
37
  digitalkin/services/__init__.py,sha256=LqGk_5DJy8Bzz62ajIq9jCeYNKQUIgtSCpafZk15FLc,910
38
- digitalkin/services/base_strategy.py,sha256=U7J09LkNvTaj4bkX-gF86iU_FcUeHK6AKp4zyUPx6eI,470
39
- digitalkin/services/services_config.py,sha256=6mNYtw07ZNZbk2b4e3xKEpYfozD_NUZyUY3iiein_10,7256
38
+ digitalkin/services/base_strategy.py,sha256=QAQnJw1BbqcYMSzwlFyhHP5juBH2WKrZzWxqDr_sDHI,638
39
+ digitalkin/services/services_config.py,sha256=4hc7-rHgSigoS3SuV0V9FReD2Dz7XoMXcD6iMBP2KKM,7391
40
40
  digitalkin/services/services_models.py,sha256=5zXkWcfKnXGwQi9sN4OAL3XrgqOcmsTl8ai5Mi4RPsw,1668
41
41
  digitalkin/services/agent/__init__.py,sha256=vJc8JN0pdtA8ecypLBeHrwAUIW6H2C8NyW-dk24rTpk,244
42
42
  digitalkin/services/agent/agent_strategy.py,sha256=42Q9RciHX6tg3CgDQkbrlIx4h_TX0WIuSpLmCjitVmA,492
43
43
  digitalkin/services/agent/default_agent.py,sha256=4N_E_eQxJGOx1KVUUg5jNOje-3ncMxF3ePB-uDuGrJc,345
44
44
  digitalkin/services/cost/__init__.py,sha256=Wi9ZB4LSXFsUYgkX-V1UJQkVXYDNDpp8q2dXccR2uRM,303
45
- digitalkin/services/cost/cost_strategy.py,sha256=KkVLT8X2EY58ul-27Gj6lFx7dsk1SZgg-UunBC0D8Ks,986
46
- digitalkin/services/cost/default_cost.py,sha256=LaTOBIdGOwh4-9vDlnefm8eKDvSWglJOS7BFLOT_G9Q,811
47
- digitalkin/services/cost/grpc_cost.py,sha256=k0Df_41dwrowBjtz11eqYQyWCMIjY-wEPEytbo2j4gE,3215
45
+ digitalkin/services/cost/cost_strategy.py,sha256=VhHeqi9WnE1yoDBBVp5qmqwIt5tTZHU6_Z_jld8CVeE,2535
46
+ digitalkin/services/cost/default_cost.py,sha256=Be_OZiFBSD4XeM--Cd1B8J2GYMhdoX5z9xAszExmKDg,3860
47
+ digitalkin/services/cost/grpc_cost.py,sha256=nGJD6vj8QSPYXTNX9-TJfqeWGHH35EsDZaEISKcznBs,6213
48
48
  digitalkin/services/filesystem/__init__.py,sha256=BhwMl_BUvM0d65fmglkp0SVwn3RfYiUOKJgIMnOCaGM,381
49
- digitalkin/services/filesystem/default_filesystem.py,sha256=tTOsBt0MSjexKgWm657y8BLppMYJtmJIVdqzsSRRaYk,7264
50
- digitalkin/services/filesystem/filesystem_strategy.py,sha256=VH93jkijFtyVA6XaH_rsfdSyeEpkqea54NAvHFLJDjM,2216
51
- digitalkin/services/filesystem/grpc_filesystem.py,sha256=KifdA_1evYA19rib5ZR5WMIrm9_jzr3PF5Sq3s2uI3Q,8180
49
+ digitalkin/services/filesystem/default_filesystem.py,sha256=PNRmLiBOkvAdo84WNZRNLYFXhSTPcJLPtfvY9vRBGPk,7396
50
+ digitalkin/services/filesystem/filesystem_strategy.py,sha256=g5duep7Trc_NWq9WNtCrnheVxfMUvX6vJyx4W0hwwiY,2348
51
+ digitalkin/services/filesystem/grpc_filesystem.py,sha256=9wZRnS6S6sssyRJEDtGDunietfxo1bJjJB2prRY155w,8320
52
52
  digitalkin/services/identity/__init__.py,sha256=InkeyLgFYYwItx8mePA8HpfacOMWZwwuc0G4pWtKq9s,270
53
53
  digitalkin/services/identity/default_identity.py,sha256=Y2auZHrGSZTIN5D8HyjLvLcNbYFM1CNUE23x7p5VIGw,386
54
54
  digitalkin/services/identity/identity_strategy.py,sha256=skappBbds1_qa0Gr24FGrNX1N0_OYhYT1Lh7dUaAirE,429
@@ -62,18 +62,18 @@ digitalkin/services/snapshot/__init__.py,sha256=Uzlnzo0CYlSpVsdiI37hW7xQk8hu3YA1
62
62
  digitalkin/services/snapshot/default_snapshot.py,sha256=Mb8QwWRsHh9I_tN0ln_ZiFa1QCZxOVWmuVLemQOTWpc,1058
63
63
  digitalkin/services/snapshot/snapshot_strategy.py,sha256=B1TU3V_k9A-OdqBkdyc41-ihnrW5Btcwd1KyQdHT46A,898
64
64
  digitalkin/services/storage/__init__.py,sha256=T-ocYLLphudkQgzvG47jBOm5GQsRFRIGA88y7Ur4akg,341
65
- digitalkin/services/storage/default_storage.py,sha256=qzLPrND92NR9hB7Ok6BF3Yxot14Efa_CHIvVte6kLsU,7817
66
- digitalkin/services/storage/grpc_storage.py,sha256=Vp1fGVGdL7Fu0EyWA_wq-1aHElMdP5knzPum22QtBF8,6936
67
- digitalkin/services/storage/storage_strategy.py,sha256=PPRFWmZTy2HEaWMGwiGzcx5NIhiGWaX3h0_NOb3a4Oo,8621
65
+ digitalkin/services/storage/default_storage.py,sha256=AWcayUtdSI1ns4loOqEYkIFiXKPV_0lclt6n94UgqSU,7883
66
+ digitalkin/services/storage/grpc_storage.py,sha256=Or94uk9KqjhLvz9IeJcuSXrNBOLczgZ1GhftQAMmn14,7003
67
+ digitalkin/services/storage/storage_strategy.py,sha256=vZWPk49AyJs7RwhSfUwDiTG1A7C4Ccd8y-VVPslyf0w,8734
68
68
  digitalkin/utils/__init__.py,sha256=sJnY-ZUgsjMfojAjONC1VN14mhgIDnzyOlGkw21rRnM,28
69
69
  digitalkin/utils/arg_parser.py,sha256=3YyI6oZhhrlTmPTrzlwpQzbCNWDFAT3pggcLxNtJoc0,4388
70
70
  digitalkin/utils/llm_ready_schema.py,sha256=JjMug_lrQllqFoanaC091VgOqwAd-_YzcpqFlS7p778,2375
71
- digitalkin-0.2.10.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
71
+ digitalkin-0.2.12.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
72
72
  modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
73
  modules/minimal_llm_module.py,sha256=W-E3OrRbAsRJ6hvSeTU8pzmacdJC_PbcWfDapRv5A1A,5617
74
- modules/storage_module.py,sha256=528tfWyRw3q5h0lUlP9Or8E7m3AnnyXezXgwisXd8BI,6399
74
+ modules/storage_module.py,sha256=U9zxEgNORR5SuunWOZvbVivzXOgMGljY-0h791bztVw,6388
75
75
  modules/text_transform_module.py,sha256=1KaA7abwxltKKtbmiW1rkkIK3BTYFPegUq54px0LOQs,7277
76
- digitalkin-0.2.10.dist-info/METADATA,sha256=u46XIpQfozHrIaBpU0t5w6R80y964eJorhuxLEyvt_A,29126
77
- digitalkin-0.2.10.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
78
- digitalkin-0.2.10.dist-info/top_level.txt,sha256=5_5e35inSM5YfWNZE21p5wGBojiVtQQML_WzbEk4BRU,31
79
- digitalkin-0.2.10.dist-info/RECORD,,
76
+ digitalkin-0.2.12.dist-info/METADATA,sha256=QYAoblLIWAUNEkl70DJzK9D0hO8EIvCLqMaMhGsu2ZU,29127
77
+ digitalkin-0.2.12.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
78
+ digitalkin-0.2.12.dist-info/top_level.txt,sha256=5_5e35inSM5YfWNZE21p5wGBojiVtQQML_WzbEk4BRU,31
79
+ digitalkin-0.2.12.dist-info/RECORD,,
modules/storage_module.py CHANGED
@@ -62,7 +62,7 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
62
62
 
63
63
  # Define services_config_params with default values
64
64
  services_config_strategies = {}
65
- services_config_params = {"storage": {"config": {"example_outputs": ExampleOutput}}, "filesystem": {"config": {}}}
65
+ services_config_params = {"storage": {"config": {"example": ExampleOutput}}, "filesystem": {"config": {}}}
66
66
 
67
67
  def __init__(self, job_id: str, mission_id: str) -> None:
68
68
  """Initialize the example module.
@@ -172,7 +172,7 @@ async def test_module() -> None:
172
172
  def test_storage_directly() -> None:
173
173
  """Test the storage service directly."""
174
174
  # Initialize storage service
175
- storage = ServicesConfig().storage(mission_id="test-mission", config={"test_table": ExampleStorage})
175
+ storage = ServicesConfig().storage(mission_id="test-mission", config={"example": ExampleStorage})
176
176
 
177
177
  # Create a test record
178
178
  storage.store("example", "test_table", {"test_key": "test_value"}, "OUTPUT")