digitalkin 0.3.2.dev2__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 (131) hide show
  1. base_server/__init__.py +1 -0
  2. base_server/mock/__init__.py +5 -0
  3. base_server/mock/mock_pb2.py +39 -0
  4. base_server/mock/mock_pb2_grpc.py +102 -0
  5. base_server/server_async_insecure.py +125 -0
  6. base_server/server_async_secure.py +143 -0
  7. base_server/server_sync_insecure.py +103 -0
  8. base_server/server_sync_secure.py +122 -0
  9. digitalkin/__init__.py +8 -0
  10. digitalkin/__version__.py +8 -0
  11. digitalkin/core/__init__.py +1 -0
  12. digitalkin/core/common/__init__.py +9 -0
  13. digitalkin/core/common/factories.py +156 -0
  14. digitalkin/core/job_manager/__init__.py +1 -0
  15. digitalkin/core/job_manager/base_job_manager.py +288 -0
  16. digitalkin/core/job_manager/single_job_manager.py +354 -0
  17. digitalkin/core/job_manager/taskiq_broker.py +311 -0
  18. digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
  19. digitalkin/core/task_manager/__init__.py +1 -0
  20. digitalkin/core/task_manager/base_task_manager.py +539 -0
  21. digitalkin/core/task_manager/local_task_manager.py +108 -0
  22. digitalkin/core/task_manager/remote_task_manager.py +87 -0
  23. digitalkin/core/task_manager/surrealdb_repository.py +266 -0
  24. digitalkin/core/task_manager/task_executor.py +249 -0
  25. digitalkin/core/task_manager/task_session.py +406 -0
  26. digitalkin/grpc_servers/__init__.py +1 -0
  27. digitalkin/grpc_servers/_base_server.py +486 -0
  28. digitalkin/grpc_servers/module_server.py +208 -0
  29. digitalkin/grpc_servers/module_servicer.py +516 -0
  30. digitalkin/grpc_servers/utils/__init__.py +1 -0
  31. digitalkin/grpc_servers/utils/exceptions.py +29 -0
  32. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
  33. digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
  34. digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
  35. digitalkin/logger.py +157 -0
  36. digitalkin/mixins/__init__.py +19 -0
  37. digitalkin/mixins/base_mixin.py +10 -0
  38. digitalkin/mixins/callback_mixin.py +24 -0
  39. digitalkin/mixins/chat_history_mixin.py +110 -0
  40. digitalkin/mixins/cost_mixin.py +76 -0
  41. digitalkin/mixins/file_history_mixin.py +93 -0
  42. digitalkin/mixins/filesystem_mixin.py +46 -0
  43. digitalkin/mixins/logger_mixin.py +51 -0
  44. digitalkin/mixins/storage_mixin.py +79 -0
  45. digitalkin/models/__init__.py +8 -0
  46. digitalkin/models/core/__init__.py +1 -0
  47. digitalkin/models/core/job_manager_models.py +36 -0
  48. digitalkin/models/core/task_monitor.py +70 -0
  49. digitalkin/models/grpc_servers/__init__.py +1 -0
  50. digitalkin/models/grpc_servers/models.py +275 -0
  51. digitalkin/models/grpc_servers/types.py +24 -0
  52. digitalkin/models/module/__init__.py +25 -0
  53. digitalkin/models/module/module.py +40 -0
  54. digitalkin/models/module/module_context.py +149 -0
  55. digitalkin/models/module/module_types.py +393 -0
  56. digitalkin/models/module/utility.py +146 -0
  57. digitalkin/models/services/__init__.py +10 -0
  58. digitalkin/models/services/cost.py +54 -0
  59. digitalkin/models/services/registry.py +42 -0
  60. digitalkin/models/services/storage.py +44 -0
  61. digitalkin/modules/__init__.py +11 -0
  62. digitalkin/modules/_base_module.py +517 -0
  63. digitalkin/modules/archetype_module.py +23 -0
  64. digitalkin/modules/tool_module.py +23 -0
  65. digitalkin/modules/trigger_handler.py +48 -0
  66. digitalkin/modules/triggers/__init__.py +12 -0
  67. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  68. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  69. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  70. digitalkin/py.typed +0 -0
  71. digitalkin/services/__init__.py +30 -0
  72. digitalkin/services/agent/__init__.py +6 -0
  73. digitalkin/services/agent/agent_strategy.py +19 -0
  74. digitalkin/services/agent/default_agent.py +13 -0
  75. digitalkin/services/base_strategy.py +22 -0
  76. digitalkin/services/communication/__init__.py +7 -0
  77. digitalkin/services/communication/communication_strategy.py +76 -0
  78. digitalkin/services/communication/default_communication.py +101 -0
  79. digitalkin/services/communication/grpc_communication.py +223 -0
  80. digitalkin/services/cost/__init__.py +14 -0
  81. digitalkin/services/cost/cost_strategy.py +100 -0
  82. digitalkin/services/cost/default_cost.py +114 -0
  83. digitalkin/services/cost/grpc_cost.py +138 -0
  84. digitalkin/services/filesystem/__init__.py +7 -0
  85. digitalkin/services/filesystem/default_filesystem.py +417 -0
  86. digitalkin/services/filesystem/filesystem_strategy.py +252 -0
  87. digitalkin/services/filesystem/grpc_filesystem.py +317 -0
  88. digitalkin/services/identity/__init__.py +6 -0
  89. digitalkin/services/identity/default_identity.py +15 -0
  90. digitalkin/services/identity/identity_strategy.py +14 -0
  91. digitalkin/services/registry/__init__.py +27 -0
  92. digitalkin/services/registry/default_registry.py +141 -0
  93. digitalkin/services/registry/exceptions.py +47 -0
  94. digitalkin/services/registry/grpc_registry.py +306 -0
  95. digitalkin/services/registry/registry_models.py +43 -0
  96. digitalkin/services/registry/registry_strategy.py +98 -0
  97. digitalkin/services/services_config.py +200 -0
  98. digitalkin/services/services_models.py +65 -0
  99. digitalkin/services/setup/__init__.py +1 -0
  100. digitalkin/services/setup/default_setup.py +219 -0
  101. digitalkin/services/setup/grpc_setup.py +343 -0
  102. digitalkin/services/setup/setup_strategy.py +145 -0
  103. digitalkin/services/snapshot/__init__.py +6 -0
  104. digitalkin/services/snapshot/default_snapshot.py +39 -0
  105. digitalkin/services/snapshot/snapshot_strategy.py +30 -0
  106. digitalkin/services/storage/__init__.py +7 -0
  107. digitalkin/services/storage/default_storage.py +228 -0
  108. digitalkin/services/storage/grpc_storage.py +214 -0
  109. digitalkin/services/storage/storage_strategy.py +273 -0
  110. digitalkin/services/user_profile/__init__.py +12 -0
  111. digitalkin/services/user_profile/default_user_profile.py +55 -0
  112. digitalkin/services/user_profile/grpc_user_profile.py +69 -0
  113. digitalkin/services/user_profile/user_profile_strategy.py +40 -0
  114. digitalkin/utils/__init__.py +29 -0
  115. digitalkin/utils/arg_parser.py +92 -0
  116. digitalkin/utils/development_mode_action.py +51 -0
  117. digitalkin/utils/dynamic_schema.py +483 -0
  118. digitalkin/utils/llm_ready_schema.py +75 -0
  119. digitalkin/utils/package_discover.py +357 -0
  120. digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
  121. digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
  122. digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
  123. digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
  124. digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
  125. modules/__init__.py +0 -0
  126. modules/cpu_intensive_module.py +280 -0
  127. modules/dynamic_setup_module.py +338 -0
  128. modules/minimal_llm_module.py +347 -0
  129. modules/text_transform_module.py +203 -0
  130. services/filesystem_module.py +200 -0
  131. services/storage_module.py +206 -0
@@ -0,0 +1,306 @@
1
+ """gRPC Registry client implementation.
2
+
3
+ This module provides a gRPC-based registry client that communicates with
4
+ the Service Provider's Registry service.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from agentic_mesh_protocol.registry.v1 import (
10
+ registry_enums_pb2,
11
+ registry_models_pb2,
12
+ registry_requests_pb2,
13
+ registry_service_pb2_grpc,
14
+ )
15
+
16
+ from digitalkin.grpc_servers.utils.exceptions import ServerError
17
+ from digitalkin.grpc_servers.utils.grpc_client_wrapper import GrpcClientWrapper
18
+ from digitalkin.grpc_servers.utils.grpc_error_handler import GrpcErrorHandlerMixin
19
+ from digitalkin.logger import logger
20
+ from digitalkin.models.grpc_servers.models import ClientConfig
21
+ from digitalkin.models.services.registry import (
22
+ ModuleInfo,
23
+ ModuleStatusInfo,
24
+ RegistryModuleStatus,
25
+ RegistryModuleType,
26
+ )
27
+ from digitalkin.services.registry.exceptions import (
28
+ RegistryModuleNotFoundError,
29
+ RegistryServiceError,
30
+ )
31
+ from digitalkin.services.registry.registry_strategy import RegistryStrategy
32
+
33
+
34
+ class GrpcRegistry(RegistryStrategy, GrpcClientWrapper, GrpcErrorHandlerMixin):
35
+ """gRPC-based registry client.
36
+
37
+ This client communicates with the Service Provider's Registry service
38
+ to perform module discovery, registration, and status management operations.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ mission_id: str,
44
+ setup_id: str,
45
+ setup_version_id: str,
46
+ client_config: ClientConfig,
47
+ config: dict[str, Any] | None = None,
48
+ ) -> None:
49
+ """Initialize the gRPC registry client."""
50
+ RegistryStrategy.__init__(self, mission_id, setup_id, setup_version_id, config)
51
+ self.service_name = "RegistryService"
52
+ self.stub = registry_service_pb2_grpc.RegistryServiceStub(self._init_channel(client_config))
53
+ logger.debug("Channel client 'Registry' initialized successfully")
54
+
55
+ @staticmethod
56
+ def _proto_to_module_info(
57
+ descriptor: registry_models_pb2.ModuleDescriptor,
58
+ ) -> ModuleInfo:
59
+ """Convert proto ModuleDescriptor to ModuleInfo.
60
+
61
+ Args:
62
+ descriptor: Proto ModuleDescriptor message.
63
+
64
+ Returns:
65
+ ModuleInfo with mapped fields.
66
+ """
67
+ type_name = registry_enums_pb2.ModuleType.Name(descriptor.module_type).removeprefix("MODULE_TYPE_")
68
+ return ModuleInfo(
69
+ module_id=descriptor.id,
70
+ module_type=RegistryModuleType[type_name],
71
+ address=descriptor.address,
72
+ port=descriptor.port,
73
+ version=descriptor.version,
74
+ name=descriptor.name,
75
+ documentation=descriptor.documentation or None,
76
+ )
77
+
78
+ def discover_by_id(self, module_id: str) -> ModuleInfo:
79
+ """Get module info by ID.
80
+
81
+ Args:
82
+ module_id: The module identifier.
83
+
84
+ Returns:
85
+ ModuleInfo with module details.
86
+
87
+ Raises:
88
+ RegistryModuleNotFoundError: If module not found.
89
+ RegistryServiceError: If gRPC call fails.
90
+ """
91
+ logger.debug("Discovering module by ID", extra={"module_id": module_id})
92
+
93
+ with self.handle_grpc_errors("GetModule", RegistryServiceError):
94
+ try:
95
+ response = self.exec_grpc_query(
96
+ "GetModule",
97
+ registry_requests_pb2.GetModuleRequest(module_id=module_id),
98
+ )
99
+ except ServerError as e:
100
+ msg = f"Failed to discover module '{module_id}': {e}"
101
+ logger.error(msg)
102
+ raise RegistryServiceError(msg) from e
103
+
104
+ if not response.id:
105
+ logger.warning("Module not found in registry", extra={"module_id": module_id})
106
+ raise RegistryModuleNotFoundError(module_id)
107
+
108
+ logger.debug(
109
+ "Module discovered",
110
+ extra={
111
+ "module_id": response.id,
112
+ "address": response.address,
113
+ "port": response.port,
114
+ },
115
+ )
116
+ return self._proto_to_module_info(response)
117
+
118
+ def search(
119
+ self,
120
+ name: str | None = None,
121
+ module_type: str | None = None,
122
+ organization_id: str | None = None,
123
+ ) -> list[ModuleInfo]:
124
+ """Search for modules by criteria.
125
+
126
+ Args:
127
+ name: Filter by name (partial match via query).
128
+ module_type: Filter by type (archetype, tool).
129
+ organization_id: Filter by organization.
130
+
131
+ Returns:
132
+ List of matching modules.
133
+
134
+ Raises:
135
+ RegistryServiceError: If gRPC call fails.
136
+ """
137
+ logger.debug(
138
+ "Searching modules",
139
+ extra={
140
+ "name": name,
141
+ "module_type": module_type,
142
+ "organization_id": organization_id,
143
+ },
144
+ )
145
+
146
+ with self.handle_grpc_errors("DiscoverModules", RegistryServiceError):
147
+ module_types = []
148
+ if module_type:
149
+ enum_val = RegistryModuleType[module_type.upper()]
150
+ module_types.append(getattr(registry_enums_pb2, f"MODULE_TYPE_{enum_val.name}"))
151
+
152
+ try:
153
+ response = self.exec_grpc_query(
154
+ "DiscoverModules",
155
+ registry_requests_pb2.DiscoverModulesRequest(
156
+ query=name or "",
157
+ organization_id=organization_id or "",
158
+ module_types=module_types,
159
+ ),
160
+ )
161
+ except ServerError as e:
162
+ msg = f"Failed to search modules: {e}"
163
+ logger.error(msg)
164
+ raise RegistryServiceError(msg) from e
165
+
166
+ logger.debug("Search returned %d modules", len(response.modules))
167
+ return [self._proto_to_module_info(m) for m in response.modules]
168
+
169
+ def get_status(self, module_id: str) -> ModuleStatusInfo:
170
+ """Get module status by fetching the module.
171
+
172
+ Args:
173
+ module_id: The module identifier.
174
+
175
+ Returns:
176
+ ModuleStatusInfo with current status.
177
+
178
+ Raises:
179
+ RegistryModuleNotFoundError: If module not found.
180
+ RegistryServiceError: If gRPC call fails.
181
+ """
182
+ logger.debug("Getting module status", extra={"module_id": module_id})
183
+
184
+ with self.handle_grpc_errors("GetModule", RegistryServiceError):
185
+ try:
186
+ response = self.exec_grpc_query(
187
+ "GetModule",
188
+ registry_requests_pb2.GetModuleRequest(module_id=module_id),
189
+ )
190
+ except ServerError as e:
191
+ msg = f"Failed to get module status for '{module_id}': {e}"
192
+ logger.error(msg)
193
+ raise RegistryServiceError(msg) from e
194
+
195
+ if not response.id:
196
+ logger.warning("Module not found in registry", extra={"module_id": module_id})
197
+ raise RegistryModuleNotFoundError(module_id)
198
+
199
+ status_name = registry_enums_pb2.ModuleStatus.Name(response.status).removeprefix("MODULE_STATUS_")
200
+ logger.debug(
201
+ "Module status retrieved",
202
+ extra={"module_id": response.id, "status": status_name},
203
+ )
204
+ return ModuleStatusInfo(
205
+ module_id=response.id,
206
+ status=RegistryModuleStatus[status_name],
207
+ )
208
+
209
+ def register(
210
+ self,
211
+ module_id: str,
212
+ address: str,
213
+ port: int,
214
+ version: str,
215
+ ) -> ModuleInfo | None:
216
+ """Register a module with the registry.
217
+
218
+ Note: The new proto only updates address/port/version for an existing module.
219
+ The module must already exist in the registry database.
220
+
221
+ Args:
222
+ module_id: Unique module identifier.
223
+ address: Network address.
224
+ port: Network port.
225
+ version: Module version.
226
+
227
+ Returns:
228
+ ModuleInfo if successful, None if module not found.
229
+
230
+ Raises:
231
+ RegistryServiceError: If gRPC call fails.
232
+ """
233
+ logger.info(
234
+ "Registering module with registry",
235
+ extra={
236
+ "module_id": module_id,
237
+ "address": address,
238
+ "port": port,
239
+ "version": version,
240
+ },
241
+ )
242
+
243
+ with self.handle_grpc_errors("RegisterModule", RegistryServiceError):
244
+ try:
245
+ response = self.exec_grpc_query(
246
+ "RegisterModule",
247
+ registry_requests_pb2.RegisterModuleRequest(
248
+ module_id=module_id,
249
+ address=address,
250
+ port=port,
251
+ version=version,
252
+ ),
253
+ )
254
+ except ServerError as e:
255
+ msg = f"Failed to register module '{module_id}': {e}"
256
+ logger.error(msg)
257
+ raise RegistryServiceError(msg) from e
258
+
259
+ if not response.module or not response.module.id:
260
+ logger.warning(
261
+ "Registry returned empty response for module registration",
262
+ extra={"module_id": module_id},
263
+ )
264
+ return None
265
+
266
+ logger.info(
267
+ "Module registered successfully",
268
+ extra={
269
+ "module_id": response.module.id,
270
+ "address": response.module.address,
271
+ "port": response.module.port,
272
+ },
273
+ )
274
+ return self._proto_to_module_info(response.module)
275
+
276
+ def heartbeat(self, module_id: str) -> RegistryModuleStatus:
277
+ """Send heartbeat to keep module active.
278
+
279
+ Args:
280
+ module_id: The module identifier.
281
+
282
+ Returns:
283
+ Current module status after heartbeat.
284
+
285
+ Raises:
286
+ RegistryServiceError: If gRPC call fails.
287
+ """
288
+ logger.debug("Sending heartbeat", extra={"module_id": module_id})
289
+
290
+ with self.handle_grpc_errors("Heartbeat", RegistryServiceError):
291
+ try:
292
+ response = self.exec_grpc_query(
293
+ "Heartbeat",
294
+ registry_requests_pb2.HeartbeatRequest(module_id=module_id),
295
+ )
296
+ except ServerError as e:
297
+ msg = f"Failed to send heartbeat for '{module_id}': {e}"
298
+ logger.error(msg)
299
+ raise RegistryServiceError(msg) from e
300
+
301
+ status_name = registry_enums_pb2.ModuleStatus.Name(response.status).removeprefix("MODULE_STATUS_")
302
+ logger.debug(
303
+ "Heartbeat response",
304
+ extra={"module_id": module_id, "status": status_name},
305
+ )
306
+ return RegistryModuleStatus[status_name]
@@ -0,0 +1,43 @@
1
+ """Registry data models.
2
+
3
+ This module contains Pydantic models for registry service data structures.
4
+ """
5
+
6
+ from enum import IntEnum
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class RegistryModuleStatus(IntEnum):
12
+ """Module status in the registry.
13
+
14
+ Maps to proto ModuleStatus enum values.
15
+ """
16
+
17
+ UNKNOWN = 0
18
+ READY = 1
19
+ ACTIVE = 2
20
+ OFFLINE = 3
21
+
22
+
23
+ class ModuleInfo(BaseModel):
24
+ """Complete module information from registry.
25
+
26
+ Maps to proto ModuleDescriptor message.
27
+ """
28
+
29
+ module_id: str
30
+ module_type: str
31
+ address: str
32
+ port: int
33
+ version: str
34
+ name: str = ""
35
+ documentation: str | None = None
36
+ status: RegistryModuleStatus | None = None
37
+
38
+
39
+ class ModuleStatusInfo(BaseModel):
40
+ """Module status response."""
41
+
42
+ module_id: str
43
+ status: RegistryModuleStatus
@@ -0,0 +1,98 @@
1
+ """Abstract base class for registry strategies."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from digitalkin.models.services.registry import (
7
+ ModuleInfo,
8
+ ModuleStatusInfo,
9
+ RegistryModuleStatus,
10
+ )
11
+ from digitalkin.services.base_strategy import BaseStrategy
12
+
13
+
14
+ class RegistryStrategy(BaseStrategy, ABC):
15
+ """Abstract base class for registry strategies.
16
+
17
+ Defines the interface for registry operations including module discovery,
18
+ registration, and status management.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ mission_id: str,
24
+ setup_id: str,
25
+ setup_version_id: str,
26
+ config: dict[str, Any] | None = None,
27
+ ) -> None:
28
+ """Initialize the strategy."""
29
+ super().__init__(mission_id, setup_id, setup_version_id)
30
+ self.config = config
31
+
32
+ @abstractmethod
33
+ def discover_by_id(self, module_id: str) -> ModuleInfo:
34
+ """Get module info by ID."""
35
+ raise NotImplementedError
36
+
37
+ @abstractmethod
38
+ def search(
39
+ self,
40
+ name: str | None = None,
41
+ module_type: str | None = None,
42
+ organization_id: str | None = None,
43
+ ) -> list[ModuleInfo]:
44
+ """Search for modules by criteria.
45
+
46
+ Args:
47
+ name: Filter by name (partial match via query).
48
+ module_type: Filter by type (archetype, tool).
49
+ organization_id: Filter by organization.
50
+
51
+ Returns:
52
+ List of matching modules.
53
+ """
54
+ raise NotImplementedError
55
+
56
+ @abstractmethod
57
+ def get_status(self, module_id: str) -> ModuleStatusInfo:
58
+ """Get module status."""
59
+ raise NotImplementedError
60
+
61
+ @abstractmethod
62
+ def register(
63
+ self,
64
+ module_id: str,
65
+ address: str,
66
+ port: int,
67
+ version: str,
68
+ ) -> ModuleInfo | None:
69
+ """Register a module with the registry.
70
+
71
+ Note: The new proto only updates address/port/version for an existing module.
72
+ The module must already exist in the registry database.
73
+
74
+ Args:
75
+ module_id: Unique module identifier.
76
+ address: Network address.
77
+ port: Network port.
78
+ version: Module version.
79
+
80
+ Returns:
81
+ ModuleInfo if successful, None otherwise.
82
+ """
83
+ raise NotImplementedError
84
+
85
+ @abstractmethod
86
+ def heartbeat(self, module_id: str) -> RegistryModuleStatus:
87
+ """Send heartbeat to keep module active.
88
+
89
+ Args:
90
+ module_id: The module identifier.
91
+
92
+ Returns:
93
+ Current module status after heartbeat.
94
+
95
+ Raises:
96
+ RegistryModuleNotFoundError: If module not found.
97
+ """
98
+ raise NotImplementedError
@@ -0,0 +1,200 @@
1
+ """Service Provider definitions."""
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from pydantic import BaseModel, Field, PrivateAttr
6
+
7
+ from digitalkin.services.agent import AgentStrategy, DefaultAgent
8
+ from digitalkin.services.communication import CommunicationStrategy, DefaultCommunication, GrpcCommunication
9
+ from digitalkin.services.cost import CostStrategy, DefaultCost, GrpcCost
10
+ from digitalkin.services.filesystem import DefaultFilesystem, FilesystemStrategy, GrpcFilesystem
11
+ from digitalkin.services.identity import DefaultIdentity, IdentityStrategy
12
+ from digitalkin.services.registry import DefaultRegistry, GrpcRegistry, RegistryStrategy
13
+ from digitalkin.services.services_models import ServicesMode, ServicesStrategy
14
+ from digitalkin.services.snapshot import DefaultSnapshot, SnapshotStrategy
15
+ from digitalkin.services.storage import DefaultStorage, GrpcStorage, StorageStrategy
16
+ from digitalkin.services.user_profile import DefaultUserProfile, GrpcUserProfile, UserProfileStrategy
17
+
18
+
19
+ class ServicesConfig(BaseModel):
20
+ """Service class describing the available services in a Module.
21
+
22
+ This class manages the strategy implementations for various services,
23
+ allowing them to be switched between local and remote modes.
24
+ """
25
+
26
+ # Mode setting for all strategies
27
+ mode: ServicesMode = Field(default=ServicesMode.LOCAL, description="The mode of the services (local or remote)")
28
+
29
+ # Strategy definitions with proper type annotations
30
+ _storage: ServicesStrategy[StorageStrategy] = PrivateAttr(
31
+ default_factory=lambda: ServicesStrategy(local=DefaultStorage, remote=GrpcStorage)
32
+ )
33
+ _config_storage: dict[str, Any | None] = PrivateAttr(default_factory=dict)
34
+ _cost: ServicesStrategy[CostStrategy] = PrivateAttr(
35
+ default_factory=lambda: ServicesStrategy(local=DefaultCost, remote=GrpcCost)
36
+ )
37
+ _config_cost: dict[str, Any | None] = PrivateAttr(default_factory=dict)
38
+ _snapshot: ServicesStrategy[SnapshotStrategy] = PrivateAttr(
39
+ default_factory=lambda: ServicesStrategy(local=DefaultSnapshot, remote=DefaultSnapshot)
40
+ )
41
+ _config_snapshot: dict[str, Any | None] = PrivateAttr(default_factory=dict)
42
+ _registry: ServicesStrategy[RegistryStrategy] = PrivateAttr(
43
+ default_factory=lambda: ServicesStrategy(local=DefaultRegistry, remote=GrpcRegistry)
44
+ )
45
+ _config_registry: dict[str, Any | None] = PrivateAttr(default_factory=dict)
46
+ _filesystem: ServicesStrategy[FilesystemStrategy] = PrivateAttr(
47
+ default_factory=lambda: ServicesStrategy(local=DefaultFilesystem, remote=GrpcFilesystem)
48
+ )
49
+ _config_filesystem: dict[str, Any | None] = PrivateAttr(default_factory=dict)
50
+ _agent: ServicesStrategy[AgentStrategy] = PrivateAttr(
51
+ default_factory=lambda: ServicesStrategy(local=DefaultAgent, remote=DefaultAgent)
52
+ )
53
+ _config_agent: dict[str, Any | None] = PrivateAttr(default_factory=dict)
54
+ _identity: ServicesStrategy[IdentityStrategy] = PrivateAttr(
55
+ default_factory=lambda: ServicesStrategy(local=DefaultIdentity, remote=DefaultIdentity)
56
+ )
57
+ _config_identity: dict[str, Any | None] = PrivateAttr(default_factory=dict)
58
+ _communication: ServicesStrategy[CommunicationStrategy] = PrivateAttr(
59
+ default_factory=lambda: ServicesStrategy(local=DefaultCommunication, remote=GrpcCommunication)
60
+ )
61
+ _config_communication: dict[str, Any | None] = PrivateAttr(default_factory=dict)
62
+ _user_profile: ServicesStrategy[UserProfileStrategy] = PrivateAttr(
63
+ default_factory=lambda: ServicesStrategy(local=DefaultUserProfile, remote=GrpcUserProfile)
64
+ )
65
+ _config_user_profile: dict[str, Any | None] = PrivateAttr(default_factory=dict)
66
+
67
+ # List of valid strategy names for validation
68
+ _valid_strategy_names: ClassVar[set[str]] = {
69
+ "storage",
70
+ "cost",
71
+ "snapshot",
72
+ "registry",
73
+ "filesystem",
74
+ "agent",
75
+ "identity",
76
+ "communication",
77
+ "user_profile",
78
+ }
79
+
80
+ def __init__(
81
+ self,
82
+ services_config_strategies: dict[str, ServicesStrategy | None] = {},
83
+ services_config_params: dict[str, dict[str, Any | None] | None] = {},
84
+ mode: ServicesMode = ServicesMode.LOCAL,
85
+ **kwargs: dict[str, Any],
86
+ ) -> None:
87
+ """Initialize the service configuration with optional strategy overrides.
88
+
89
+ Args:
90
+ services_config_strategies: Dictionary mapping service names to strategy implementations
91
+ services_config_params: Dictionary mapping service names to configuration parameters
92
+ mode: The mode of the services (local or remote)
93
+ **kwargs: Additional keyword arguments passed to the parent class constructor
94
+ """
95
+ super().__init__(**kwargs)
96
+ self.mode = mode
97
+ # Apply any strategy overrides
98
+ if services_config_strategies:
99
+ for name, strategy in services_config_strategies.items():
100
+ if strategy is not None and name in self._valid_strategy_names:
101
+ setattr(self, f"_{name}", strategy)
102
+
103
+ for name in self.valid_strategy_names():
104
+ setattr(self, f"_config_{name}", services_config_params.get(name, {}))
105
+
106
+ @classmethod
107
+ def valid_strategy_names(cls) -> set[str]:
108
+ """Get the list of valid strategy names.
109
+
110
+ Returns:
111
+ The set of valid strategy names.
112
+ """
113
+ return cls._valid_strategy_names
114
+
115
+ def get_strategy_config(self, name: str) -> dict[str, Any]:
116
+ """Get the configuration for a specific strategy.
117
+
118
+ Args:
119
+ name: The name of the strategy to retrieve the configuration for
120
+
121
+ Returns:
122
+ The configuration for the specified strategy, or None if not found
123
+ """
124
+ return getattr(self, f"_config_{name}", {})
125
+
126
+ def init_strategy(self, name: str, mission_id: str, setup_id: str, setup_version_id: str) -> ServicesStrategy:
127
+ """Initialize a specific strategy.
128
+
129
+ Args:
130
+ name: The name of the strategy to initialize
131
+ mission_id: The ID of the mission this strategy is associated with
132
+ setup_id: The setup ID for the strategy
133
+ setup_version_id: The setup version ID for the strategy
134
+
135
+ Returns:
136
+ The initialized strategy instance
137
+
138
+ Raises:
139
+ ValueError: If the strategy is not found
140
+ """
141
+ strategy_type = getattr(self, name, None)
142
+ if strategy_type is None:
143
+ msg = f"Strategy {name} not found in ServicesConfig."
144
+ raise ValueError(msg)
145
+
146
+ # Instantiate the strategy with the mission ID, setup version ID, and configuration
147
+ return strategy_type(mission_id, setup_id, setup_version_id, **self.get_strategy_config(name) or {})
148
+
149
+ @property
150
+ def storage(self) -> type[StorageStrategy]:
151
+ """Get the storage service strategy class based on the current mode."""
152
+ return self._storage[self.mode.value]
153
+
154
+ @property
155
+ def cost(self) -> type[CostStrategy]:
156
+ """Get the cost service strategy class based on the current mode."""
157
+ return self._cost[self.mode.value]
158
+
159
+ @property
160
+ def snapshot(self) -> type[SnapshotStrategy]:
161
+ """Get the snapshot service strategy class based on the current mode."""
162
+ return self._snapshot[self.mode.value]
163
+
164
+ @property
165
+ def registry(self) -> type[RegistryStrategy]:
166
+ """Get the registry service strategy class based on the current mode."""
167
+ return self._registry[self.mode.value]
168
+
169
+ @property
170
+ def filesystem(self) -> type[FilesystemStrategy]:
171
+ """Get the filesystem service strategy class based on the current mode."""
172
+ return self._filesystem[self.mode.value]
173
+
174
+ @property
175
+ def agent(self) -> type[AgentStrategy]:
176
+ """Get the agent service strategy class based on the current mode."""
177
+ return self._agent[self.mode.value]
178
+
179
+ @property
180
+ def identity(self) -> type[IdentityStrategy]:
181
+ """Get the identity service strategy class based on the current mode."""
182
+ return self._identity[self.mode.value]
183
+
184
+ @property
185
+ def communication(self) -> type[CommunicationStrategy]:
186
+ """Get the communication service strategy class based on the current mode."""
187
+ return self._communication[self.mode.value]
188
+
189
+ @property
190
+ def user_profile(self) -> type[UserProfileStrategy]:
191
+ """Get the user_profile service strategy class based on the current mode."""
192
+ return self._user_profile[self.mode.value]
193
+
194
+ def update_mode(self, mode: ServicesMode) -> None:
195
+ """Update the strategy mode.
196
+
197
+ Parameters:
198
+ mode: The new mode to use for all strategies
199
+ """
200
+ self.mode = mode