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,317 @@
1
+ """gRPC filesystem implementation."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from agentic_mesh_protocol.filesystem.v1 import filesystem_pb2, filesystem_service_pb2_grpc
6
+ from google.protobuf import struct_pb2
7
+ from google.protobuf.json_format import MessageToDict
8
+
9
+ from digitalkin.grpc_servers.utils.grpc_client_wrapper import GrpcClientWrapper
10
+ from digitalkin.grpc_servers.utils.grpc_error_handler import GrpcErrorHandlerMixin
11
+ from digitalkin.logger import logger
12
+ from digitalkin.models.grpc_servers.models import ClientConfig
13
+ from digitalkin.services.filesystem.filesystem_strategy import (
14
+ FileFilter,
15
+ FilesystemRecord,
16
+ FilesystemServiceError,
17
+ FilesystemStrategy,
18
+ UploadFileData,
19
+ )
20
+
21
+
22
+ class GrpcFilesystem(FilesystemStrategy, GrpcClientWrapper, GrpcErrorHandlerMixin):
23
+ """Default state filesystem strategy."""
24
+
25
+ @staticmethod
26
+ def _file_type_to_enum(file_type: str) -> filesystem_pb2.FileType:
27
+ """Convert a file type string to a FileType enum.
28
+
29
+ Args:
30
+ file_type: The file type string to convert
31
+
32
+ Returns:
33
+ filesystem_pb2.FileType: The converted file type enum
34
+ """
35
+ if not file_type.upper().startswith("FILE_TYPE_"):
36
+ file_type = f"FILE_TYPE_{file_type.upper()}"
37
+ try:
38
+ return getattr(filesystem_pb2.FileType, file_type.upper())
39
+ except AttributeError:
40
+ return filesystem_pb2.FileType.FILE_TYPE_UNSPECIFIED
41
+
42
+ @staticmethod
43
+ def _file_status_to_enum(file_status: str) -> filesystem_pb2.FileStatus:
44
+ """Convert a file status string to a FileStatus enum.
45
+
46
+ Args:
47
+ file_status: The file status string to convert
48
+
49
+ Returns:
50
+ filesystem_pb2.FileStatus: The converted file status enum
51
+ """
52
+ if not file_status.upper().startswith("FILE_STATUS_"):
53
+ file_status = f"FILE_STATUS_{file_status.upper()}"
54
+ try:
55
+ return getattr(filesystem_pb2.FileStatus, file_status.upper())
56
+ except AttributeError:
57
+ return filesystem_pb2.FileStatus.FILE_STATUS_UNSPECIFIED
58
+
59
+ @staticmethod
60
+ def _file_proto_to_data(file: filesystem_pb2.File) -> FilesystemRecord:
61
+ """Convert a File proto message to FilesystemRecord.
62
+
63
+ Args:
64
+ file: The File proto message to convert
65
+
66
+ Returns:
67
+ FilesystemRecord: The converted data
68
+ """
69
+ return FilesystemRecord(
70
+ id=file.file_id,
71
+ context=file.context,
72
+ name=file.name,
73
+ file_type=filesystem_pb2.FileType.Name(file.file_type),
74
+ content_type=file.content_type,
75
+ size_bytes=file.size_bytes,
76
+ checksum=file.checksum,
77
+ metadata=MessageToDict(file.metadata),
78
+ storage_uri=file.storage_uri,
79
+ file_url=file.file_url,
80
+ status=filesystem_pb2.FileStatus.Name(file.status),
81
+ content=file.content,
82
+ )
83
+
84
+ def _filter_to_proto(self, filters: FileFilter) -> filesystem_pb2.FileFilter:
85
+ """Convert a FileFilter to a FileFilter proto message.
86
+
87
+ Args:
88
+ filters: The FileFilter to convert
89
+
90
+ Returns:
91
+ filesystem_pb2.FileFilter: The converted FileFilter proto message
92
+ """
93
+ return filesystem_pb2.FileFilter(
94
+ **filters.model_dump(exclude={"file_types", "status"}),
95
+ file_types=[self._file_type_to_enum(file_type) for file_type in filters.file_types]
96
+ if filters.file_types
97
+ else None,
98
+ status=self._file_status_to_enum(filters.status) if filters.status else None,
99
+ )
100
+
101
+ def __init__(
102
+ self,
103
+ mission_id: str,
104
+ setup_id: str,
105
+ setup_version_id: str,
106
+ client_config: ClientConfig,
107
+ config: dict[str, Any] | None = None,
108
+ ) -> None:
109
+ """Initialize the gRPC filesystem strategy.
110
+
111
+ Args:
112
+ mission_id: The ID of the mission this strategy is associated with
113
+ setup_id: The ID of the setup
114
+ setup_version_id: The ID of the setup version this strategy is associated with
115
+ client_config: Configuration for the gRPC client connection
116
+ config: Configuration for the filesystem strategy
117
+ """
118
+ super().__init__(mission_id, setup_id, setup_version_id, config)
119
+ self.service_name = "FilesystemService"
120
+ channel = self._init_channel(client_config)
121
+ self.stub = filesystem_service_pb2_grpc.FilesystemServiceStub(channel)
122
+ logger.debug("Channel client 'Filesystem' initialized successfully")
123
+
124
+ def upload_files(
125
+ self,
126
+ files: list[UploadFileData],
127
+ ) -> tuple[list[FilesystemRecord], int, int]:
128
+ """Upload multiple files to the filesystem.
129
+
130
+ Args:
131
+ files: List of tuples containing (content, name, file_type, content_type, metadata, replace_if_exists)
132
+
133
+ Returns:
134
+ tuple[list[FilesystemRecord], int, int]: List of uploaded files, total uploaded count, total failed count
135
+ """
136
+ logger.debug("Uploading %d files", len(files))
137
+ with self.handle_grpc_errors("UploadFiles", FilesystemServiceError):
138
+ upload_files: list[filesystem_pb2.UploadFileData] = []
139
+ for file in files:
140
+ metadata_struct: struct_pb2.Struct | None = None
141
+ if file.metadata:
142
+ metadata_struct = struct_pb2.Struct()
143
+ metadata_struct.update(file.metadata)
144
+ upload_files.append(
145
+ filesystem_pb2.UploadFileData(
146
+ context=self.mission_id,
147
+ name=file.name,
148
+ file_type=self._file_type_to_enum(file.file_type),
149
+ content_type=file.content_type or "application/octet-stream",
150
+ content=file.content,
151
+ metadata=metadata_struct,
152
+ status=filesystem_pb2.FileStatus.FILE_STATUS_UPLOADING,
153
+ replace_if_exists=file.replace_if_exists,
154
+ )
155
+ )
156
+ request = filesystem_pb2.UploadFilesRequest(files=upload_files)
157
+ response: filesystem_pb2.UploadFilesResponse = self.exec_grpc_query("UploadFiles", request)
158
+ results = [self._file_proto_to_data(result.file) for result in response.results if result.HasField("file")]
159
+ logger.debug("Uploaded files: %s", results)
160
+ return results, response.total_uploaded, response.total_failed
161
+
162
+ def get_file(
163
+ self,
164
+ file_id: str,
165
+ context: Literal["mission", "setup"] = "mission",
166
+ *,
167
+ include_content: bool = False,
168
+ ) -> FilesystemRecord:
169
+ """Get a file from the filesystem.
170
+
171
+ Args:
172
+ file_id: The ID of the file to be retrieved
173
+ context: The context of the files (mission or setup)
174
+ include_content: Whether to include file content in response
175
+
176
+ Returns:
177
+ FilesystemRecord: Metadata about the retrieved file
178
+
179
+ Raises:
180
+ FilesystemServiceError: If there is an error retrieving the file
181
+ """
182
+ match context:
183
+ case "setup":
184
+ context_id = self.setup_id
185
+ case "mission":
186
+ context_id = self.mission_id
187
+ with self.handle_grpc_errors("GetFile", FilesystemServiceError):
188
+ request = filesystem_pb2.GetFileRequest(
189
+ context=context_id,
190
+ file_id=file_id,
191
+ include_content=include_content,
192
+ )
193
+
194
+ response: filesystem_pb2.GetFileResponse = self.exec_grpc_query("GetFile", request)
195
+
196
+ return self._file_proto_to_data(response.file)
197
+
198
+ def update_file(
199
+ self,
200
+ file_id: str,
201
+ content: bytes | None = None,
202
+ file_type: Literal[
203
+ "UNSPECIFIED",
204
+ "DOCUMENT",
205
+ "IMAGE",
206
+ "VIDEO",
207
+ "AUDIO",
208
+ "ARCHIVE",
209
+ "CODE",
210
+ "OTHER",
211
+ ]
212
+ | None = None,
213
+ content_type: str | None = None,
214
+ metadata: dict[str, Any] | None = None,
215
+ new_name: str | None = None,
216
+ status: str | None = None,
217
+ ) -> FilesystemRecord:
218
+ """Update a file in the filesystem.
219
+
220
+ Args:
221
+ file_id: The id of the file to be updated
222
+ content: Optional new content of the file
223
+ file_type: Optional new type of data
224
+ content_type: Optional new MIME type
225
+ metadata: Optional new metadata (will merge with existing)
226
+ new_name: Optional new name for the file
227
+ status: Optional new status for the file
228
+
229
+ Returns:
230
+ FilesystemRecord: Metadata about the updated file
231
+
232
+ Raises:
233
+ FilesystemServiceError: If there is an error during update
234
+ """
235
+ with self.handle_grpc_errors("UpdateFile", FilesystemServiceError):
236
+ request = filesystem_pb2.UpdateFileRequest(
237
+ context=self.mission_id,
238
+ file_id=file_id,
239
+ content=content,
240
+ file_type=self._file_type_to_enum(file_type) if file_type else None,
241
+ content_type=content_type,
242
+ new_name=new_name,
243
+ status=self._file_status_to_enum(status) if status else None,
244
+ )
245
+
246
+ if metadata:
247
+ request.metadata.update(metadata)
248
+
249
+ response: filesystem_pb2.UpdateFileResponse = self.exec_grpc_query("UpdateFile", request)
250
+ return self._file_proto_to_data(response.result.file)
251
+
252
+ def delete_files(
253
+ self,
254
+ filters: FileFilter,
255
+ *,
256
+ permanent: bool = False,
257
+ force: bool = False,
258
+ ) -> tuple[dict[str, bool], int, int]:
259
+ """Delete multiple files from the filesystem.
260
+
261
+ Args:
262
+ filters: Filter criteria for the files
263
+ permanent: Whether to permanently delete the files
264
+ force: Whether to force delete even if files are in use
265
+
266
+ Returns:
267
+ tuple[dict[str, bool], int, int]: Results per file, total deleted count, total failed count
268
+ """
269
+ with self.handle_grpc_errors("DeleteFiles", FilesystemServiceError):
270
+ request = filesystem_pb2.DeleteFilesRequest(
271
+ context=self.mission_id,
272
+ filters=self._filter_to_proto(filters),
273
+ permanent=permanent,
274
+ force=force,
275
+ )
276
+
277
+ response: filesystem_pb2.DeleteFilesResponse = self.exec_grpc_query("DeleteFiles", request)
278
+ return dict(response.results), response.total_deleted, response.total_failed
279
+
280
+ def get_files(
281
+ self,
282
+ filters: FileFilter,
283
+ *,
284
+ list_size: int = 100,
285
+ offset: int = 0,
286
+ order: str | None = None,
287
+ include_content: bool = False,
288
+ ) -> tuple[list[FilesystemRecord], int]:
289
+ """Get multiple files from the filesystem.
290
+
291
+ Args:
292
+ filters: Filter criteria for the files
293
+ list_size: Number of files to return per page
294
+ offset: Offset to start from
295
+ order: Field to order results by
296
+ include_content: Whether to include file content in response
297
+
298
+ Returns:
299
+ tuple[list[FilesystemRecord], int]: List of files and total count
300
+ """
301
+ match filters.context:
302
+ case "setup":
303
+ context_id = self.setup_id
304
+ case "mission":
305
+ context_id = self.mission_id
306
+ with self.handle_grpc_errors("GetFiles", FilesystemServiceError):
307
+ request = filesystem_pb2.GetFilesRequest(
308
+ context=context_id,
309
+ filters=self._filter_to_proto(filters),
310
+ include_content=include_content,
311
+ list_size=list_size,
312
+ offset=offset,
313
+ order=order,
314
+ )
315
+ response: filesystem_pb2.GetFilesResponse = self.exec_grpc_query("GetFiles", request)
316
+
317
+ return [self._file_proto_to_data(file) for file in response.files], response.total_count
@@ -0,0 +1,6 @@
1
+ """This module is responsible for handling the identity service."""
2
+
3
+ from digitalkin.services.identity.default_identity import DefaultIdentity
4
+ from digitalkin.services.identity.identity_strategy import IdentityStrategy
5
+
6
+ __all__ = ["DefaultIdentity", "IdentityStrategy"]
@@ -0,0 +1,15 @@
1
+ """Default identity."""
2
+
3
+ from digitalkin.services.identity.identity_strategy import IdentityStrategy
4
+
5
+
6
+ class DefaultIdentity(IdentityStrategy):
7
+ """DefaultIdentity is the default identity strategy."""
8
+
9
+ async def get_identity(self) -> str: # noqa: PLR6301
10
+ """Get the identity.
11
+
12
+ Returns:
13
+ str: The identity
14
+ """
15
+ return "default_identity"
@@ -0,0 +1,14 @@
1
+ """This module contains the abstract base class for identity strategies."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from digitalkin.services.base_strategy import BaseStrategy
6
+
7
+
8
+ class IdentityStrategy(BaseStrategy, ABC):
9
+ """IdentityStrategy is the abstract base class for all identity strategies."""
10
+
11
+ @abstractmethod
12
+ async def get_identity(self) -> str:
13
+ """Get the identity."""
14
+ raise NotImplementedError
@@ -0,0 +1,27 @@
1
+ """This module is responsible for handling the registry service."""
2
+
3
+ from digitalkin.models.services.registry import (
4
+ ModuleInfo,
5
+ ModuleStatusInfo,
6
+ RegistryModuleStatus,
7
+ RegistryModuleType,
8
+ )
9
+ from digitalkin.services.registry.default_registry import DefaultRegistry
10
+ from digitalkin.services.registry.exceptions import (
11
+ RegistryModuleNotFoundError,
12
+ RegistryServiceError,
13
+ )
14
+ from digitalkin.services.registry.grpc_registry import GrpcRegistry
15
+ from digitalkin.services.registry.registry_strategy import RegistryStrategy
16
+
17
+ __all__ = [
18
+ "DefaultRegistry",
19
+ "GrpcRegistry",
20
+ "ModuleInfo",
21
+ "ModuleStatusInfo",
22
+ "RegistryModuleNotFoundError",
23
+ "RegistryModuleStatus",
24
+ "RegistryModuleType",
25
+ "RegistryServiceError",
26
+ "RegistryStrategy",
27
+ ]
@@ -0,0 +1,141 @@
1
+ """Default registry implementation."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from digitalkin.models.services.registry import (
6
+ ModuleInfo,
7
+ ModuleStatusInfo,
8
+ RegistryModuleStatus,
9
+ RegistryModuleType,
10
+ )
11
+ from digitalkin.services.registry.exceptions import RegistryModuleNotFoundError
12
+ from digitalkin.services.registry.registry_strategy import RegistryStrategy
13
+
14
+
15
+ class DefaultRegistry(RegistryStrategy):
16
+ """Default registry strategy using in-memory storage."""
17
+
18
+ _modules: ClassVar[dict[str, ModuleInfo]] = {}
19
+
20
+ def discover_by_id(self, module_id: str) -> ModuleInfo:
21
+ """Get module info by ID.
22
+
23
+ Args:
24
+ module_id: The module identifier.
25
+
26
+ Returns:
27
+ ModuleInfo with module details.
28
+
29
+ Raises:
30
+ RegistryModuleNotFoundError: If module not found.
31
+ """
32
+ if module_id not in self._modules:
33
+ raise RegistryModuleNotFoundError(module_id)
34
+ return self._modules[module_id]
35
+
36
+ def search(
37
+ self,
38
+ name: str | None = None,
39
+ module_type: str | None = None,
40
+ organization_id: str | None = None, # noqa: ARG002
41
+ ) -> list[ModuleInfo]:
42
+ """Search for modules by criteria.
43
+
44
+ Args:
45
+ name: Filter by name (partial match).
46
+ module_type: Filter by type (archetype, tool).
47
+ organization_id: Filter by organization (not used in local storage).
48
+
49
+ Returns:
50
+ List of matching modules.
51
+ """
52
+ results = list(self._modules.values())
53
+
54
+ if name:
55
+ results = [m for m in results if name in m.name]
56
+
57
+ if module_type:
58
+ results = [m for m in results if m.module_type == module_type]
59
+
60
+ return results
61
+
62
+ def get_status(self, module_id: str) -> ModuleStatusInfo:
63
+ """Get module status.
64
+
65
+ Args:
66
+ module_id: The module identifier.
67
+
68
+ Returns:
69
+ ModuleStatusInfo with current status.
70
+
71
+ Raises:
72
+ RegistryModuleNotFoundError: If module not found.
73
+ """
74
+ if module_id not in self._modules:
75
+ raise RegistryModuleNotFoundError(module_id)
76
+
77
+ module = self._modules[module_id]
78
+ return ModuleStatusInfo(
79
+ module_id=module_id,
80
+ status=module.status or RegistryModuleStatus.UNSPECIFIED,
81
+ )
82
+
83
+ def register(
84
+ self,
85
+ module_id: str,
86
+ address: str,
87
+ port: int,
88
+ version: str,
89
+ ) -> ModuleInfo | None:
90
+ """Register a module with the registry.
91
+
92
+ Note: Updates existing module or creates new one in local storage.
93
+
94
+ Args:
95
+ module_id: Unique module identifier.
96
+ address: Network address.
97
+ port: Network port.
98
+ version: Module version.
99
+
100
+ Returns:
101
+ ModuleInfo if successful, None otherwise.
102
+ """
103
+ existing = self._modules.get(module_id)
104
+ self._modules[module_id] = ModuleInfo(
105
+ module_id=module_id,
106
+ module_type=existing.module_type if existing else RegistryModuleType.UNSPECIFIED,
107
+ address=address,
108
+ port=port,
109
+ version=version,
110
+ name=existing.name if existing else module_id,
111
+ status=RegistryModuleStatus.ACTIVE,
112
+ )
113
+ return self._modules[module_id]
114
+
115
+ def heartbeat(self, module_id: str) -> RegistryModuleStatus:
116
+ """Send heartbeat to keep module active.
117
+
118
+ Args:
119
+ module_id: The module identifier.
120
+
121
+ Returns:
122
+ Current module status after heartbeat.
123
+
124
+ Raises:
125
+ RegistryModuleNotFoundError: If module not found.
126
+ """
127
+ if module_id not in self._modules:
128
+ raise RegistryModuleNotFoundError(module_id)
129
+
130
+ module = self._modules[module_id]
131
+ # Update status to ACTIVE on heartbeat
132
+ self._modules[module_id] = ModuleInfo(
133
+ module_id=module.module_id,
134
+ module_type=module.module_type,
135
+ address=module.address,
136
+ port=module.port,
137
+ version=module.version,
138
+ name=module.name,
139
+ status=RegistryModuleStatus.ACTIVE,
140
+ )
141
+ return RegistryModuleStatus.ACTIVE
@@ -0,0 +1,47 @@
1
+ """Registry-specific exceptions.
2
+
3
+ This module contains custom exceptions for registry service operations.
4
+ """
5
+
6
+
7
+ class RegistryServiceError(Exception):
8
+ """Base exception for registry service errors."""
9
+
10
+
11
+ class RegistryModuleNotFoundError(RegistryServiceError):
12
+ """Raised when a module is not found in the registry."""
13
+
14
+ def __init__(self, module_id: str) -> None:
15
+ """Initialize the exception.
16
+
17
+ Args:
18
+ module_id: The ID of the module that was not found.
19
+ """
20
+ self.module_id = module_id
21
+ super().__init__(f"Module '{module_id}' not found in registry")
22
+
23
+
24
+ class ModuleAlreadyExistsError(RegistryServiceError):
25
+ """Raised when attempting to register an already-registered module."""
26
+
27
+ def __init__(self, module_id: str) -> None:
28
+ """Initialize the exception.
29
+
30
+ Args:
31
+ module_id: The ID of the module that already exists.
32
+ """
33
+ self.module_id = module_id
34
+ super().__init__(f"Module '{module_id}' already registered")
35
+
36
+
37
+ class InvalidStatusError(RegistryServiceError):
38
+ """Raised when an invalid status is provided."""
39
+
40
+ def __init__(self, status: int) -> None:
41
+ """Initialize the exception.
42
+
43
+ Args:
44
+ status: The invalid status value.
45
+ """
46
+ self.status = status
47
+ super().__init__(f"Invalid module status: {status}")