digitalkin 0.1.1__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 (59) hide show
  1. digitalkin/__init__.py +18 -0
  2. digitalkin/__version__.py +11 -0
  3. digitalkin/grpc/__init__.py +31 -0
  4. digitalkin/grpc/_base_server.py +488 -0
  5. digitalkin/grpc/module_server.py +233 -0
  6. digitalkin/grpc/module_servicer.py +304 -0
  7. digitalkin/grpc/registry_server.py +63 -0
  8. digitalkin/grpc/registry_servicer.py +451 -0
  9. digitalkin/grpc/utils/exceptions.py +33 -0
  10. digitalkin/grpc/utils/factory.py +178 -0
  11. digitalkin/grpc/utils/models.py +169 -0
  12. digitalkin/grpc/utils/types.py +24 -0
  13. digitalkin/logger.py +17 -0
  14. digitalkin/models/__init__.py +11 -0
  15. digitalkin/models/module/__init__.py +5 -0
  16. digitalkin/models/module/module.py +31 -0
  17. digitalkin/models/services/__init__.py +6 -0
  18. digitalkin/models/services/cost.py +53 -0
  19. digitalkin/models/services/storage.py +10 -0
  20. digitalkin/modules/__init__.py +7 -0
  21. digitalkin/modules/_base_module.py +177 -0
  22. digitalkin/modules/archetype_module.py +14 -0
  23. digitalkin/modules/job_manager.py +158 -0
  24. digitalkin/modules/tool_module.py +14 -0
  25. digitalkin/modules/trigger_module.py +14 -0
  26. digitalkin/py.typed +0 -0
  27. digitalkin/services/__init__.py +28 -0
  28. digitalkin/services/agent/__init__.py +6 -0
  29. digitalkin/services/agent/agent_strategy.py +22 -0
  30. digitalkin/services/agent/default_agent.py +16 -0
  31. digitalkin/services/cost/__init__.py +6 -0
  32. digitalkin/services/cost/cost_strategy.py +15 -0
  33. digitalkin/services/cost/default_cost.py +13 -0
  34. digitalkin/services/default_service.py +13 -0
  35. digitalkin/services/development_service.py +10 -0
  36. digitalkin/services/filesystem/__init__.py +6 -0
  37. digitalkin/services/filesystem/default_filesystem.py +29 -0
  38. digitalkin/services/filesystem/filesystem_strategy.py +31 -0
  39. digitalkin/services/identity/__init__.py +6 -0
  40. digitalkin/services/identity/default_identity.py +15 -0
  41. digitalkin/services/identity/identity_strategy.py +12 -0
  42. digitalkin/services/registry/__init__.py +6 -0
  43. digitalkin/services/registry/default_registry.py +13 -0
  44. digitalkin/services/registry/registry_strategy.py +17 -0
  45. digitalkin/services/service_provider.py +27 -0
  46. digitalkin/services/snapshot/__init__.py +6 -0
  47. digitalkin/services/snapshot/default_snapshot.py +39 -0
  48. digitalkin/services/snapshot/snapshot_strategy.py +31 -0
  49. digitalkin/services/storage/__init__.py +6 -0
  50. digitalkin/services/storage/default_storage.py +91 -0
  51. digitalkin/services/storage/grpc_storage.py +207 -0
  52. digitalkin/services/storage/storage_strategy.py +42 -0
  53. digitalkin/utils/__init__.py +1 -0
  54. digitalkin/utils/arg_parser.py +136 -0
  55. digitalkin-0.1.1.dist-info/METADATA +588 -0
  56. digitalkin-0.1.1.dist-info/RECORD +59 -0
  57. digitalkin-0.1.1.dist-info/WHEEL +5 -0
  58. digitalkin-0.1.1.dist-info/licenses/LICENSE +430 -0
  59. digitalkin-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,233 @@
1
+ """Module gRPC server implementation for DigitalKin."""
2
+
3
+ import logging
4
+ import uuid
5
+
6
+ import grpc
7
+
8
+ from digitalkin.grpc._base_server import BaseServer
9
+ from digitalkin.grpc.module_servicer import ModuleServicer
10
+ from digitalkin.grpc.utils.exceptions import ServerError
11
+ from digitalkin.grpc.utils.models import ModuleServerConfig, SecurityMode
12
+ from digitalkin.modules._base_module import BaseModule
13
+
14
+ from digitalkin_proto.digitalkin.module.v2 import module_service_pb2, module_service_pb2_grpc
15
+ from digitalkin_proto.digitalkin.module_registry.v2 import (
16
+ metadata_pb2,
17
+ module_registry_service_pb2_grpc,
18
+ registration_pb2,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ModuleServer(BaseServer):
25
+ """gRPC server for a DigitalKin module.
26
+
27
+ This server exposes the module's functionality through the ModuleService gRPC interface.
28
+ It can optionally register itself with a ModuleRegistry server.
29
+
30
+ Attributes:
31
+ module: The module instance being served.
32
+ config: Server configuration.
33
+ module_servicer: The gRPC servicer handling module requests.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ module_class: type[BaseModule],
39
+ config: ModuleServerConfig,
40
+ ) -> None:
41
+ """Initialize the module server.
42
+
43
+ Args:
44
+ module_class: The module instance to be served.
45
+ config: Server configuration including registry address if auto-registration is desired.
46
+ """
47
+ super().__init__(config)
48
+ self.module_class = module_class
49
+ self.config = config
50
+ self.module_servicer: ModuleServicer | None = None
51
+
52
+ def _register_servicers(self) -> None:
53
+ """Register the module servicer with the gRPC server.
54
+
55
+ Raises:
56
+ RuntimeError: No registered server
57
+ """
58
+ if self.server is None:
59
+ msg = "Server must be created before registering servicers"
60
+ raise RuntimeError(msg)
61
+
62
+ logger.info("Registering module servicer for %s", self.module_class.__name__)
63
+ self.module_servicer = ModuleServicer(self.module_class)
64
+ self.register_servicer(
65
+ self.module_servicer,
66
+ module_service_pb2_grpc.add_ModuleServiceServicer_to_server,
67
+ service_descriptor=module_service_pb2.DESCRIPTOR,
68
+ )
69
+ logger.info("Registered Async Greeter servicer")
70
+
71
+ def start(self) -> None:
72
+ """Start the module server and register with the registry if configured."""
73
+ logger.critical(self.config)
74
+ super().start()
75
+
76
+ logger.critical(self.config)
77
+ # If a registry address is provided, register the module
78
+ if self.config.registry_address:
79
+ try:
80
+ self._register_with_registry()
81
+ except Exception:
82
+ logger.exception("Failed to register with registry")
83
+
84
+ async def start_async(self) -> None:
85
+ """Start the module server and register with the registry if configured."""
86
+ logger.critical(self.config)
87
+ await super().start_async()
88
+
89
+ logger.critical(self.config)
90
+ # If a registry address is provided, register the module
91
+ if self.config.registry_address:
92
+ try:
93
+ self._register_with_registry()
94
+ except Exception:
95
+ logger.exception("Failed to register with registry")
96
+
97
+ def stop(self, grace: float | None = None) -> None:
98
+ """Stop the module server and deregister from the registry if needed."""
99
+ # If registered with a registry, deregister
100
+ if self.config.registry_address:
101
+ try:
102
+ self._deregister_from_registry()
103
+ except ServerError:
104
+ logger.exception("Failed to deregister from registry")
105
+
106
+ super().stop(grace)
107
+
108
+ def _register_with_registry(self) -> None:
109
+ """Register this module with the registry server.
110
+
111
+ Raises:
112
+ ServerError: If communication with the registry server fails.
113
+ """
114
+ logger.info("Registering module with registry at %s", self.config.registry_address)
115
+
116
+ # Create appropriate channel based on security mode
117
+ channel = self._create_registry_channel()
118
+
119
+ with channel:
120
+ # Create a stub (client)
121
+ stub = module_registry_service_pb2_grpc.ModuleRegistryServiceStub(channel)
122
+
123
+ # Determine module type
124
+ module_type = self._determine_module_type()
125
+
126
+ metadata = metadata_pb2.Metadata(
127
+ name=self.module_class.metadata["name"],
128
+ tags=[metadata_pb2.Tag(tag=tag) for tag in self.module_class.metadata["tags"]],
129
+ description=self.module_class.metadata["description"],
130
+ )
131
+
132
+ self.module_class.metadata["module_id"] = f"{self.module_class.metadata['name']}:{uuid.uuid4()}"
133
+ # Create registration request
134
+ request = registration_pb2.RegisterRequest(
135
+ module_id=self.module_class.metadata["module_id"],
136
+ version=self.module_class.metadata["version"],
137
+ module_type=module_type,
138
+ address=self.config.address,
139
+ metadata=metadata,
140
+ )
141
+
142
+ try:
143
+ # Call the register method
144
+ logger.info(
145
+ "Request sent to registry for module: %s:%s",
146
+ self.module_class.metadata["name"],
147
+ self.module_class.metadata["module_id"],
148
+ )
149
+ response = stub.RegisterModule(request)
150
+
151
+ if response.success:
152
+ logger.info("Module registered successfully")
153
+ else:
154
+ logger.error("Module registration failed")
155
+ except grpc.RpcError:
156
+ logger.exception("RPC error during registration:")
157
+ raise ServerError
158
+
159
+ def _deregister_from_registry(self) -> None:
160
+ """Deregister this module from the registry server.
161
+
162
+ Raises:
163
+ ServerError: If communication with the registry server fails.
164
+ """
165
+ logger.info("Deregistering module from registry at %s", self.config.registry_address)
166
+
167
+ # Create appropriate channel based on security mode
168
+ channel = self._create_registry_channel()
169
+
170
+ with channel:
171
+ # Create a stub (client)
172
+ stub = module_registry_service_pb2_grpc.ModuleRegistryServiceStub(channel)
173
+
174
+ # Create deregistration request
175
+ request = registration_pb2.DeregisterRequest(
176
+ module_id=self.module_class.metadata["module_id"],
177
+ )
178
+ try:
179
+ # Call the deregister method
180
+ response = stub.DeregisterModule(request)
181
+
182
+ if response.success:
183
+ logger.info("Module deregistered successfull")
184
+ else:
185
+ logger.error("Module deregistration failed")
186
+ except grpc.RpcError:
187
+ logger.exception("RPC error during deregistration")
188
+ raise ServerError
189
+
190
+ def _create_registry_channel(self) -> grpc.Channel:
191
+ """Create an appropriate channel to the registry server.
192
+
193
+ Returns:
194
+ A gRPC channel for communication with the registry.
195
+
196
+ Raises:
197
+ ValueError: If credentials are required but not provided.
198
+ """
199
+ if self.config.security == SecurityMode.SECURE and self.config.credentials:
200
+ # Secure channel
201
+ """TODO: use Path(self.config.credentials.server_cert_path).read_bytes()"""
202
+ with open(self.config.credentials.server_cert_path, "rb") as cert_file: # noqa: FURB101
203
+ certificate_chain = cert_file.read()
204
+
205
+ root_certificates = None
206
+ if self.config.credentials.root_cert_path:
207
+ with open(self.config.credentials.root_cert_path, "rb") as root_cert_file: # noqa: FURB101
208
+ root_certificates = root_cert_file.read()
209
+
210
+ # Create channel credentials
211
+ channel_credentials = grpc.ssl_channel_credentials(root_certificates=root_certificates or certificate_chain)
212
+
213
+ return grpc.secure_channel(self.config.registry_address, channel_credentials)
214
+ # Insecure channel
215
+ return grpc.insecure_channel(self.config.registry_address)
216
+
217
+ def _determine_module_type(self) -> str:
218
+ """Determine the module type based on its class.
219
+
220
+ Returns:
221
+ A string representing the module type.
222
+ """
223
+ module_type = "UNKNOWN"
224
+ class_name = self.module_class.__name__
225
+
226
+ if class_name == "ToolModule":
227
+ module_type = "TOOL"
228
+ elif class_name == "TriggerModule":
229
+ module_type = "TRIGGER"
230
+ elif class_name == "ArchetypeModule":
231
+ module_type = "KIN"
232
+
233
+ return module_type
@@ -0,0 +1,304 @@
1
+ """Module servicer implementation for DigitalKin."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from collections.abc import AsyncGenerator
6
+ from typing import Any
7
+
8
+ import grpc
9
+ from digitalkin_proto.digitalkin.module.v2 import (
10
+ information_pb2,
11
+ lifecycle_pb2,
12
+ module_service_pb2_grpc,
13
+ monitoring_pb2,
14
+ )
15
+ from google.protobuf import json_format, struct_pb2
16
+
17
+ from digitalkin.models.module.module import ModuleStatus
18
+ from digitalkin.modules._base_module import BaseModule
19
+ from digitalkin.modules.job_manager import JobManager
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer):
25
+ """Implementation of the ModuleService.
26
+
27
+ This servicer handles interactions with a DigitalKin module.
28
+
29
+ Attributes:
30
+ module: The module instance being served.
31
+ active_jobs: Dictionary tracking active module jobs.
32
+ """
33
+
34
+ def __init__(self, module_class: type[BaseModule]) -> None:
35
+ """Initialize the module servicer.
36
+
37
+ Args:
38
+ module_class: The module type to serve.
39
+ """
40
+ super().__init__()
41
+ self.queue: asyncio.Queue = asyncio.Queue()
42
+ self.module_class = module_class
43
+ self.job_manager = JobManager(module_class)
44
+
45
+ async def add_to_queue(self, job_id: str, output_data: dict[str, Any]) -> None:
46
+ """Callback used to add the output data to the queue of messages."""
47
+ logger.info("JOB: %s added an output_data: %s", job_id, output_data)
48
+ await self.queue.put({job_id: output_data})
49
+
50
+ async def StartModule( # noqa: N802
51
+ self,
52
+ request: lifecycle_pb2.StartModuleRequest,
53
+ context: grpc.aio.ServicerContext,
54
+ ) -> AsyncGenerator[lifecycle_pb2.StartModuleResponse, Any]:
55
+ """Start a module execution.
56
+
57
+ Args:
58
+ request: Iterator of start module requests.
59
+ context: The gRPC context.
60
+
61
+ Yields:
62
+ Responses during module execution.
63
+ """
64
+ logger.info("StartModule called for module: '%s'", self.module_class.__name__)
65
+ # Process the module input
66
+ input_data = dict(request.input.items())
67
+ setup_data = self.module_class.storage.get(
68
+ table="setups",
69
+ data={
70
+ "keys": [
71
+ # remove prefix 'setups:'
72
+ request.setup_id[7:],
73
+ ],
74
+ },
75
+ )[0]
76
+
77
+ # setup_id should be use to request a precise setup from the module
78
+ # Create a job for this execution
79
+ job_id, module = await self.job_manager.create_job(
80
+ input_data,
81
+ setup_data,
82
+ callback=self.add_to_queue,
83
+ )
84
+
85
+ while module.status == ModuleStatus.RUNNING or not self.queue.empty():
86
+ output_data = await self.queue.get()
87
+ if output_data.get("error", None) is not None:
88
+ context.set_code(output_data["error"]["code"])
89
+ context.set_details(output_data["error"]["error_message"])
90
+ yield lifecycle_pb2.StartModuleResponse(success=False)
91
+ return
92
+ else:
93
+ # TODO: add a check for the job_id / error handling
94
+ output_proto = {key: str(value) for key, value in output_data[job_id].items()}
95
+ yield lifecycle_pb2.StartModuleResponse(
96
+ success=True,
97
+ output=output_proto,
98
+ job_id=job_id,
99
+ )
100
+
101
+ async def StopModule( # noqa: N802
102
+ self,
103
+ request: lifecycle_pb2.StopModuleRequest,
104
+ context: grpc.ServicerContext,
105
+ ) -> lifecycle_pb2.StopModuleResponse:
106
+ """Stop a running module execution.
107
+
108
+ Args:
109
+ request: The stop module request.
110
+ context: The gRPC context.
111
+
112
+ Returns:
113
+ A response indicating success or failure.
114
+ """
115
+ logger.info("StopModule called for module: '%s'", self.module_class.__name__)
116
+
117
+ job_id = request.job_id
118
+ if job_id not in self.job_manager.modules:
119
+ message = f"Job {job_id} not found"
120
+ logger.warning(message)
121
+ context.set_code(grpc.StatusCode.NOT_FOUND)
122
+ context.set_details(message)
123
+ return lifecycle_pb2.StopModuleResponse(success=False)
124
+
125
+ # Update the job status
126
+ await self.job_manager.modules[job_id].stop()
127
+
128
+ logger.info("Job %s stopped successfully", job_id)
129
+ return lifecycle_pb2.StopModuleResponse(success=True)
130
+
131
+ def GetModuleStatus( # noqa: N802
132
+ self,
133
+ request: monitoring_pb2.GetModuleStatusRequest,
134
+ context: grpc.ServicerContext,
135
+ ) -> monitoring_pb2.GetModuleStatusResponse:
136
+ """Get the status of a module.
137
+
138
+ Args:
139
+ request: The get module status request.
140
+ context: The gRPC context.
141
+
142
+ Returns:
143
+ A response with the module status.
144
+ """
145
+ logger.info("GetModuleStatus called for module: '%s'", self.module_class.__name__)
146
+
147
+ # If job_id is specified, get status for that job
148
+ if request.job_id:
149
+ if request.job_id not in self.job_manager.modules:
150
+ message = f"Job {request.job_id} not found"
151
+ logger.warning(message)
152
+ context.set_code(grpc.StatusCode.NOT_FOUND)
153
+ context.set_details(message)
154
+ return monitoring_pb2.GetModuleStatusResponse()
155
+
156
+ status = self.job_manager.modules[request.job_id].status
157
+ logger.info("Job %s status: '%s'", request.job_id, status)
158
+ return monitoring_pb2.GetModuleStatusResponse(
159
+ success=True,
160
+ status=status.name,
161
+ job_id=request.job_id,
162
+ )
163
+
164
+ logger.info("Job %s status: '%s'", request.job_id, ModuleStatus.NOT_FOUND)
165
+ return monitoring_pb2.GetModuleStatusResponse(
166
+ success=False,
167
+ status=ModuleStatus.NOT_FOUND.name,
168
+ job_id=request.job_id,
169
+ )
170
+
171
+ def GetModuleJobs( # noqa: N802
172
+ self,
173
+ request: monitoring_pb2.GetModuleJobsRequest, # noqa: ARG002
174
+ context: grpc.ServicerContext, # noqa: ARG002
175
+ ) -> monitoring_pb2.GetModuleJobsResponse:
176
+ """Get information about the module's jobs.
177
+
178
+ Args:
179
+ request: The get module jobs request.
180
+ context: The gRPC context.
181
+
182
+ Returns:
183
+ A response with information about active jobs.
184
+ """
185
+ logger.info("GetModuleJobs called for module: '%s'", self.module_class.__name__)
186
+
187
+ # Create job info objects for each active job
188
+ return monitoring_pb2.GetModuleJobsResponse(
189
+ jobs=[
190
+ monitoring_pb2.JobInfo(
191
+ job_id=job_id,
192
+ job_status=job_data.status.name,
193
+ )
194
+ for job_id, job_data in self.job_manager.modules.items()
195
+ ],
196
+ )
197
+
198
+ def GetModuleInput( # noqa: N802
199
+ self,
200
+ request: information_pb2.GetModuleInputRequest,
201
+ context: grpc.ServicerContext,
202
+ ) -> information_pb2.GetModuleInputResponse:
203
+ """Get information about the module's expected input.
204
+
205
+ Args:
206
+ request: The get module input request.
207
+ context: The gRPC context.
208
+
209
+ Returns:
210
+ A response with the module's input schema.
211
+ """
212
+ logger.info("GetModuleInput called for module: '%s'", self.module_class.__name__)
213
+
214
+ # Get input schema if available
215
+ try:
216
+ # Convert schema to proto format
217
+ input_schema_proto = self.module_class.get_input_format(request.llm_format)
218
+ input_format_struct = json_format.Parse(
219
+ text=input_schema_proto,
220
+ message=struct_pb2.Struct(), # pylint: disable=no-member
221
+ ignore_unknown_fields=True,
222
+ )
223
+ except NotImplementedError as e:
224
+ logger.warning(e)
225
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
226
+ context.set_details(e)
227
+ return information_pb2.GetModuleInputResponse()
228
+
229
+ return information_pb2.GetModuleInputResponse(
230
+ success=True,
231
+ input_schema=input_format_struct,
232
+ )
233
+
234
+ def GetModuleOutput( # noqa: N802
235
+ self,
236
+ request: information_pb2.GetModuleOutputRequest,
237
+ context: grpc.ServicerContext,
238
+ ) -> information_pb2.GetModuleOutputResponse:
239
+ """Get information about the module's expected output.
240
+
241
+ Args:
242
+ request: The get module output request.
243
+ context: The gRPC context.
244
+
245
+ Returns:
246
+ A response with the module's output schema.
247
+ """
248
+ logger.info("GetModuleOutput called for module: '%s'", self.module_class.__name__)
249
+
250
+ # Get output schema if available
251
+ try:
252
+ # Convert schema to proto format
253
+ output_schema_proto = self.module_class.get_output_format(request.llm_format)
254
+ output_format_struct = json_format.Parse(
255
+ text=output_schema_proto,
256
+ message=struct_pb2.Struct(), # pylint: disable=no-member
257
+ ignore_unknown_fields=True,
258
+ )
259
+ except NotImplementedError as e:
260
+ logger.warning(e)
261
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
262
+ context.set_details(e)
263
+ return information_pb2.GetModuleOutputResponse()
264
+
265
+ return information_pb2.GetModuleOutputResponse(
266
+ success=True,
267
+ output_schema=output_format_struct,
268
+ )
269
+
270
+ def GetModuleSetup( # noqa: N802
271
+ self,
272
+ request: information_pb2.GetModuleSetupRequest,
273
+ context: grpc.ServicerContext,
274
+ ) -> information_pb2.GetModuleSetupResponse:
275
+ """Get information about the module's setup and configuration.
276
+
277
+ Args:
278
+ request: The get module setup request.
279
+ context: The gRPC context.
280
+
281
+ Returns:
282
+ A response with the module's setup information.
283
+ """
284
+ logger.info("GetModuleSetup called for module: '%s'", self.module_class.__name__)
285
+
286
+ # Get setup schema if available
287
+ try:
288
+ # Convert schema to proto format
289
+ setup_schema_proto = self.module_class.get_setup_format(request.llm_format)
290
+ setup_format_struct = json_format.Parse(
291
+ text=setup_schema_proto,
292
+ message=struct_pb2.Struct(), # pylint: disable=no-member
293
+ ignore_unknown_fields=True,
294
+ )
295
+ except NotImplementedError as e:
296
+ logger.warning(e)
297
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
298
+ context.set_details(e)
299
+ return information_pb2.GetModuleSetupResponse()
300
+
301
+ return information_pb2.GetModuleSetupResponse(
302
+ success=True,
303
+ setup_schema=setup_format_struct,
304
+ )
@@ -0,0 +1,63 @@
1
+ """Registry gRPC server implementation for DigitalKin."""
2
+
3
+ import logging
4
+
5
+ from digitalkin_proto.digitalkin.module_registry.v2 import module_registry_service_pb2_grpc
6
+
7
+ from digitalkin.grpc._base_server import BaseServer
8
+ from digitalkin.grpc.registry_servicer import RegistryModule, RegistryServicer
9
+ from digitalkin.grpc.utils.models import RegistryServerConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class RegistryServer(BaseServer):
15
+ """gRPC server for DigitalKin module registry.
16
+
17
+ This server implements the ModuleRegistryService which allows modules to register
18
+ themselves and be discovered by other components in the system.
19
+
20
+ Attributes:
21
+ config: Server configuration.
22
+ registry_servicer: The gRPC servicer handling registry requests.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ config: RegistryServerConfig,
28
+ ) -> None:
29
+ """Initialize the registry server.
30
+
31
+ Args:
32
+ config: Server configuration.
33
+ """
34
+ super().__init__(config)
35
+ self.config = config
36
+ self.registry_servicer: RegistryServicer | None = None
37
+
38
+ def _register_servicers(self) -> None:
39
+ """Register the registry servicer with the gRPC server.
40
+
41
+ Raises:
42
+ RuntimeError: If server is not registered during server creation
43
+ """
44
+ if self.server is None:
45
+ msg = "Server must be created before registering servicers"
46
+ raise RuntimeError(msg)
47
+
48
+ logger.info("Registering module registry servicer")
49
+ self.registry_servicer = RegistryServicer()
50
+ module_registry_service_pb2_grpc.add_ModuleRegistryServiceServicer_to_server(
51
+ self.registry_servicer, self.server
52
+ )
53
+ self._servicers.append(self.registry_servicer)
54
+
55
+ def get_registered_modules(self) -> list[RegistryModule]:
56
+ """Get a list of all registered modules.
57
+
58
+ Returns:
59
+ A list of module information objects.
60
+ """
61
+ if self.registry_servicer:
62
+ return list(self.registry_servicer.registered_modules.values())
63
+ return []