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
digitalkin/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """DigitalKin SDK!
2
+
3
+ This package implements the DigitalKin agentic mesh standards.
4
+ """
5
+
6
+ from digitalkin.__version__ import __version__
7
+ from digitalkin.models import Module
8
+
9
+ # Import key components to make them available at the package level
10
+ from digitalkin.modules import ArchetypeModule, ToolModule, TriggerModule
11
+
12
+ __all__ = [
13
+ "ArchetypeModule",
14
+ "Module",
15
+ "ToolModule",
16
+ "TriggerModule",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,11 @@
1
+ """Version information."""
2
+
3
+ import os
4
+ import sys
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
8
+ try:
9
+ __version__ = version("digitalkin")
10
+ except PackageNotFoundError:
11
+ __version__ = "0.1.1"
@@ -0,0 +1,31 @@
1
+ r"""This package contains the gRPC server and client implementations.
2
+
3
+ ```shell
4
+ digitalkin/grpc/
5
+ ├── __init__.py
6
+ ├── base_server.py # Base server implementation with common functionality
7
+ ├── module_server.py # Module-specific server implementation
8
+ ├── registry_server.py # Registry-specific server implementation
9
+ ├── module_servicer.py # gRPC servicer for Module service
10
+ ├── registry_servicer.py # gRPC servicer for Registry service
11
+ ├── client/ # Client libraries for connecting to servers
12
+ │ ├── __init__.py
13
+ │ ├── module_client.py
14
+ │ └── registry_client.py
15
+ └── utils/ # Utility functions
16
+ ├── __init__.py
17
+ └── server_utils.py # Common server utilities
18
+ ```
19
+
20
+ /!\ Circular import based on this:
21
+
22
+ from ._base_server import BaseServer
23
+ from .module_servicer import ModuleServicer
24
+ from .registry_servicer import RegistryServicer
25
+
26
+ __all__ = [
27
+ "BaseServer",
28
+ "ModuleServicer",
29
+ "RegistryServicer",
30
+ ]
31
+ """
@@ -0,0 +1,488 @@
1
+ """Base gRPC server implementation for DigitalKin."""
2
+
3
+ import abc
4
+ import asyncio
5
+ import logging
6
+ from collections.abc import Callable
7
+ from concurrent import futures
8
+ from pathlib import Path
9
+ from typing import Any, cast
10
+
11
+ import grpc
12
+ from grpc import aio as grpc_aio
13
+
14
+ from digitalkin.grpc.utils.exceptions import (
15
+ ConfigurationError,
16
+ ReflectionError,
17
+ SecurityError,
18
+ ServerStateError,
19
+ ServicerError,
20
+ )
21
+ from digitalkin.grpc.utils.models import SecurityMode, ServerConfig, ServerMode
22
+ from digitalkin.grpc.utils.types import GrpcServer, ServiceDescriptor, T
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class BaseServer(abc.ABC):
28
+ """Base class for gRPC servers in DigitalKin.
29
+
30
+ This class provides the foundation for both synchronous and asynchronous gRPC
31
+ servers used in the DigitalKin ecosystem. It supports both secure and insecure
32
+ communication modes.
33
+
34
+ Attributes:
35
+ config: The server configuration.
36
+ server: The gRPC server instance (either sync or async).
37
+ _servicers: List of registered servicers.
38
+ _service_names: List of service names for reflection.
39
+ _health_servicer: Optional health check servicer.
40
+ """
41
+
42
+ def __init__(self, config: ServerConfig) -> None:
43
+ """Initialize the base gRPC server.
44
+
45
+ Args:
46
+ config: The server configuration.
47
+ """
48
+ self.config = config
49
+ self.server: GrpcServer | None = None
50
+ self._servicers: list[Any] = []
51
+ self._service_names: list[str] = [] # Track service names for reflection
52
+ self._health_servicer: Any = None # For health checking
53
+
54
+ def register_servicer(
55
+ self,
56
+ servicer: T,
57
+ add_to_server_fn: Callable[[T, GrpcServer], None],
58
+ service_descriptor: ServiceDescriptor | None = None,
59
+ service_names: list[str] | None = None,
60
+ ) -> None:
61
+ """Register a servicer with the gRPC server and track it for reflection.
62
+
63
+ Args:
64
+ servicer: The servicer implementation instance
65
+ add_to_server_fn: The function to add the servicer to the server
66
+ service_descriptor: Optional service descriptor (pb2 DESCRIPTOR)
67
+ service_names: Optional explicit list of service full names
68
+
69
+ Raises:
70
+ ServicerError: If the server is not created before calling
71
+ """
72
+ if self.server is None:
73
+ msg = "Server must be created before registering servicers"
74
+ raise ServicerError(msg)
75
+
76
+ # Register the servicer
77
+ try:
78
+ add_to_server_fn(servicer, self.server)
79
+ self._servicers.append(servicer)
80
+ except Exception as e:
81
+ msg = f"Failed to register servicer: {e}"
82
+ raise ServicerError(msg) from e
83
+
84
+ # Add service names from explicit list
85
+ if service_names:
86
+ for name in service_names:
87
+ if name not in self._service_names:
88
+ self._service_names.append(name)
89
+ logger.debug("Registered explicit service name for reflection: %s", name)
90
+
91
+ # If a descriptor is provided, extract service names
92
+ if service_descriptor and hasattr(service_descriptor, "services_by_name"):
93
+ for service_name in service_descriptor.services_by_name:
94
+ full_name = service_descriptor.services_by_name[service_name].full_name # ignore: PLC0206
95
+ if full_name not in self._service_names:
96
+ self._service_names.append(full_name)
97
+ logger.debug("Registered service name from descriptor: %s", full_name)
98
+
99
+ @abc.abstractmethod
100
+ def _register_servicers(self) -> None:
101
+ """Register servicers with the gRPC server.
102
+
103
+ This method should be implemented by subclasses to register
104
+ the appropriate servicers for their specific functionality.
105
+
106
+ Raises:
107
+ ServicerError: If the server is not created before calling this method.
108
+ """
109
+
110
+ def _add_reflection(self) -> None:
111
+ """Add reflection service to the gRPC server if enabled.
112
+
113
+ Raises:
114
+ ReflectionError: If reflection initialization fails.
115
+ """
116
+ if not self.config.enable_reflection or self.server is None or not self._service_names:
117
+ return
118
+
119
+ try:
120
+ # Import here to avoid dependency if not used
121
+ from grpc_reflection.v1alpha import reflection # noqa: PLC0415
122
+
123
+ # Get all registered service names
124
+ service_names = self._service_names.copy()
125
+
126
+ # Add the reflection service name
127
+ reflection_service = reflection.SERVICE_NAME
128
+ service_names.append(reflection_service)
129
+
130
+ # Register services with the reflection service
131
+ # This creates a dynamic file descriptor database that can respond to
132
+ # reflection queries with detailed service information
133
+ reflection.enable_server_reflection(service_names, self.server)
134
+
135
+ logger.info("Added gRPC reflection service with services: %s", service_names)
136
+ except ImportError:
137
+ logger.warning("Could not enable reflection: grpcio-reflection package not installed")
138
+ except Exception as e:
139
+ error_msg = f"Failed to enable reflection: {e}"
140
+ logger.warning(error_msg)
141
+ raise ReflectionError(error_msg) from e
142
+
143
+ def _add_health_service(self) -> None:
144
+ """Add health checking service to the gRPC server.
145
+
146
+ The health service allows clients to check server status.
147
+ """
148
+ if self.server is None:
149
+ return
150
+
151
+ try:
152
+ # Import here to avoid dependency if not used
153
+ from grpc_health.v1 import health_pb2, health_pb2_grpc # noqa: PLC0415
154
+ from grpc_health.v1.health import HealthServicer # noqa: PLC0415
155
+
156
+ # Create health servicer
157
+ health_servicer = HealthServicer()
158
+
159
+ # Register health servicer
160
+ health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self.server)
161
+
162
+ # Add service name to reflection list
163
+ if health_pb2.DESCRIPTOR.services_by_name:
164
+ service_name = health_pb2.DESCRIPTOR.services_by_name["Health"].full_name
165
+ if service_name not in self._service_names:
166
+ self._service_names.append(service_name)
167
+
168
+ logger.info("Added gRPC health checking service")
169
+
170
+ # Set all services as SERVING
171
+ for service_name in self._service_names:
172
+ health_servicer.set(service_name, health_pb2.HealthCheckResponse.SERVING)
173
+
174
+ # Set overall service status
175
+ health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
176
+
177
+ # Store reference to health servicer
178
+ self._health_servicer = health_servicer
179
+
180
+ except ImportError:
181
+ logger.warning("Could not enable health service: grpcio-health-checking package not installed")
182
+ except Exception as e:
183
+ logger.warning("Failed to enable health service: %s", e)
184
+
185
+ def _create_server(self) -> GrpcServer:
186
+ """Create a gRPC server instance based on the configuration.
187
+
188
+ Returns:
189
+ A configured gRPC server instance.
190
+
191
+ Raises:
192
+ ConfigurationError: If the server configuration is invalid.
193
+ """
194
+ try:
195
+ # Create the server based on mode
196
+ if self.config.mode == ServerMode.ASYNC:
197
+ server = grpc_aio.server(options=self.config.server_options)
198
+ else:
199
+ server = grpc.server(
200
+ futures.ThreadPoolExecutor(max_workers=self.config.max_workers),
201
+ options=self.config.server_options,
202
+ )
203
+
204
+ # Add the appropriate port
205
+ if self.config.security == SecurityMode.SECURE:
206
+ self._add_secure_port(server)
207
+ else:
208
+ self._add_insecure_port(server)
209
+
210
+ except Exception as e:
211
+ msg = f"Failed to create server: {e}"
212
+ raise ConfigurationError(msg) from e
213
+ else:
214
+ return server
215
+
216
+ def _add_secure_port(self, server: GrpcServer) -> None:
217
+ """Add a secure port to the server.
218
+
219
+ Args:
220
+ server: The gRPC server to add the port to.
221
+
222
+ Raises:
223
+ SecurityError: If credentials are not configured correctly.
224
+ """
225
+ if not self.config.credentials:
226
+ msg = "Credentials must be provided for secure server"
227
+ raise SecurityError(msg)
228
+
229
+ try:
230
+ # Read key and certificate files
231
+ private_key = Path(self.config.credentials.server_key_path).read_bytes()
232
+ certificate_chain = Path(self.config.credentials.server_cert_path).read_bytes()
233
+
234
+ # Read root certificate if provided
235
+ root_certificates = None
236
+ if self.config.credentials.root_cert_path:
237
+ root_certificates = Path(self.config.credentials.root_cert_path).read_bytes()
238
+ except OSError as e:
239
+ msg = f"Failed to read credential files: {e}"
240
+ raise SecurityError(msg) from e
241
+
242
+ try:
243
+ # Create server credentials
244
+ server_credentials = grpc.ssl_server_credentials(
245
+ [(private_key, certificate_chain)],
246
+ root_certificates=root_certificates,
247
+ require_client_auth=(root_certificates is not None),
248
+ )
249
+
250
+ # Add secure port to server
251
+ if self.config.mode == ServerMode.ASYNC:
252
+ async_server = cast("grpc_aio.Server", server)
253
+ async_server.add_secure_port(self.config.address, server_credentials)
254
+ else:
255
+ sync_server = cast("grpc.Server", server)
256
+ sync_server.add_secure_port(self.config.address, server_credentials)
257
+
258
+ logger.info("Added secure port %s", self.config.address)
259
+ except Exception as e:
260
+ msg = f"Failed to configure secure port: {e}"
261
+ raise SecurityError(msg) from e
262
+
263
+ def _add_insecure_port(self, server: GrpcServer) -> None:
264
+ """Add an insecure port to the server.
265
+
266
+ Args:
267
+ server: The gRPC server to add the port to.
268
+
269
+ Raises:
270
+ ConfigurationError: If adding the insecure port fails.
271
+ """
272
+ try:
273
+ if self.config.mode == ServerMode.ASYNC:
274
+ async_server = cast("grpc_aio.Server", server)
275
+ async_server.add_insecure_port(self.config.address)
276
+ else:
277
+ sync_server = cast("grpc.Server", server)
278
+ sync_server.add_insecure_port(self.config.address)
279
+
280
+ logger.info("Added insecure port %s", self.config.address)
281
+ except Exception as e:
282
+ msg = f"Failed to add insecure port: {e}"
283
+ raise ConfigurationError(msg) from e
284
+
285
+ def start(self) -> None:
286
+ """Start the gRPC server.
287
+
288
+ If using async mode, this will use the event loop to start the server.
289
+ If using sync mode, this will start the server in a non-blocking way.
290
+
291
+ Raises:
292
+ ServerStateError: If the server fails to start.
293
+ """
294
+ self.server = self._create_server()
295
+ self._register_servicers()
296
+
297
+ # Add health service
298
+ self._add_health_service()
299
+
300
+ # Add reflection if enabled
301
+ self._add_reflection()
302
+
303
+ # Start the server
304
+ logger.info("Starting gRPC server on %s", self.config.address)
305
+ try:
306
+ if self.config.mode == ServerMode.ASYNC:
307
+ # For async server, use the event loop
308
+ loop = asyncio.get_event_loop()
309
+ if loop.is_closed():
310
+ loop = asyncio.new_event_loop()
311
+ asyncio.set_event_loop(loop)
312
+ loop.run_until_complete(self._start_async())
313
+ else:
314
+ # For sync server, directly call start
315
+ sync_server = cast("grpc.Server", self.server)
316
+ sync_server.start()
317
+ logger.info("✅ gRPC server started on %s", self.config.address)
318
+ except Exception as e:
319
+ logger.exception("❎ Error starting server")
320
+ msg = f"Failed to start server: {e}"
321
+ raise ServerStateError(msg) from e
322
+
323
+ async def _start_async(self) -> None:
324
+ """Start the async gRPC server.
325
+
326
+ Raises:
327
+ ServerStateError: If the server is not created.
328
+ """
329
+ if self.server is None:
330
+ msg = "Server is not created"
331
+ raise ServerStateError(msg)
332
+
333
+ async_server = cast("grpc_aio.Server", self.server)
334
+ await async_server.start()
335
+
336
+ async def start_async(self) -> None:
337
+ """Start the gRPC server asynchronously.
338
+
339
+ This method should be used directly in an async context.
340
+
341
+ Raises:
342
+ ServerStateError: If the server fails to start.
343
+ """
344
+ self.server = self._create_server()
345
+ self._register_servicers()
346
+
347
+ # Add health service
348
+ self._add_health_service()
349
+
350
+ # Add reflection if enabled
351
+ self._add_reflection()
352
+
353
+ # Start the server
354
+ logger.info("Starting gRPC server on %s", self.config.address)
355
+ try:
356
+ if self.config.mode == ServerMode.ASYNC:
357
+ await self._start_async()
358
+ else:
359
+ # For sync server in async context
360
+ sync_server = cast("grpc.Server", self.server)
361
+ sync_server.start()
362
+ logger.info("✅ gRPC server started on %s", self.config.address)
363
+ except Exception as e:
364
+ logger.exception("❎ Error starting server")
365
+ msg = f"Failed to start server: {e}"
366
+ raise ServerStateError(msg) from e
367
+
368
+ def stop(self, grace: float | None = None) -> None:
369
+ """Stop the gRPC server.
370
+
371
+ Args:
372
+ grace: Optional grace period in seconds for existing RPCs to complete.
373
+ """
374
+ if self.server is None:
375
+ logger.warning("Attempted to stop server, but no server is running")
376
+ return
377
+
378
+ logger.info("Stopping gRPC server...")
379
+ if self.config.mode == ServerMode.ASYNC:
380
+ # We'll use a different approach that works whether we're in a running event loop or not
381
+ try:
382
+ # Get the current event loop
383
+ loop = asyncio.get_event_loop()
384
+
385
+ if loop.is_running():
386
+ # If we're in a running event loop, we can't run_until_complete
387
+ # Just warn the user they should use stop_async
388
+ logger.warning(
389
+ "Called stop() on async server from a running event loop. "
390
+ "This might not fully shut down the server. "
391
+ "Use await stop_async() in async contexts instead."
392
+ )
393
+ # Set server to None to avoid further operations
394
+ self.server = None
395
+ logger.info("✅ gRPC server marked as stopped")
396
+ return
397
+ # If not in a running event loop, use run_until_complete
398
+ loop.run_until_complete(self._stop_async(grace))
399
+ except RuntimeError:
400
+ # Event loop issues - try with a new loop
401
+ logger.info("Creating new event loop for shutdown")
402
+ try:
403
+ new_loop = asyncio.new_event_loop()
404
+ asyncio.set_event_loop(new_loop)
405
+ new_loop.run_until_complete(self._stop_async(grace))
406
+ finally:
407
+ new_loop.close()
408
+ else:
409
+ # For sync server, we can just call stop
410
+ sync_server = cast("grpc.Server", self.server)
411
+ sync_server.stop(grace=grace)
412
+
413
+ logger.info("✅ gRPC server stopped")
414
+ self.server = None
415
+
416
+ async def _stop_async(self, grace: float | None = None) -> None:
417
+ """Stop the async gRPC server.
418
+
419
+ Args:
420
+ grace: Optional grace period in seconds for existing RPCs to complete.
421
+ """
422
+ if self.server is None:
423
+ return
424
+
425
+ async_server = cast("grpc_aio.Server", self.server)
426
+ await async_server.stop(grace=grace)
427
+
428
+ async def stop_async(self, grace: float | None = None) -> None:
429
+ """Stop the gRPC server asynchronously.
430
+
431
+ This method should be used in async contexts.
432
+
433
+ Args:
434
+ grace: Optional grace period in seconds for existing RPCs to complete.
435
+ """
436
+ if self.server is None:
437
+ logger.warning("Attempted to stop server, but no server is running")
438
+ return
439
+
440
+ logger.info("Stopping gRPC server asynchronously...")
441
+ if self.config.mode == ServerMode.ASYNC:
442
+ await self._stop_async(grace)
443
+ else:
444
+ # For sync server, we can just call stop
445
+ sync_server = cast("grpc.Server", self.server)
446
+ sync_server.stop(grace=grace)
447
+
448
+ logger.info("✅ gRPC server stopped")
449
+ self.server = None
450
+
451
+ def wait_for_termination(self) -> None:
452
+ """Wait for the server to terminate.
453
+
454
+ In synchronous mode, this blocks until the server is terminated.
455
+ In asynchronous mode, a warning is logged suggesting to use `await_termination`.
456
+ """
457
+ if self.server is None:
458
+ logger.warning("Attempted to wait for termination, but no server is running")
459
+ return
460
+
461
+ if self.config.mode == ServerMode.SYNC:
462
+ # For sync server
463
+ sync_server = cast("grpc.Server", self.server)
464
+ sync_server.wait_for_termination()
465
+ else:
466
+ # For async server, the caller should use await_termination instead
467
+ logger.warning(
468
+ "Called wait_for_termination on async server. Use await_termination instead for async servers.",
469
+ )
470
+
471
+ async def await_termination(self) -> None:
472
+ """Wait for the async server to terminate.
473
+
474
+ This method should only be used with async servers.
475
+ """
476
+ if self.config.mode == ServerMode.SYNC:
477
+ logger.warning(
478
+ "Called await_termination on sync server. Use wait_for_termination instead for sync servers.",
479
+ )
480
+ return
481
+
482
+ if self.server is None:
483
+ logger.warning("Attempted to await termination, but no server is running")
484
+ return
485
+
486
+ # For async server
487
+ async_server = cast("grpc_aio.Server", self.server)
488
+ await async_server.wait_for_termination()