digitalkin 0.2.16__tar.gz → 0.2.17__tar.gz

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 (97) hide show
  1. {digitalkin-0.2.16 → digitalkin-0.2.17}/PKG-INFO +1 -1
  2. {digitalkin-0.2.16 → digitalkin-0.2.17}/pyproject.toml +2 -2
  3. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/__version__.py +1 -1
  4. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/module_server.py +6 -6
  5. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/module_servicer.py +1 -0
  6. digitalkin-0.2.17/src/digitalkin/models/module/__init__.py +26 -0
  7. digitalkin-0.2.17/src/digitalkin/models/module/module_context.py +24 -0
  8. digitalkin-0.2.17/src/digitalkin/models/module/module_types.py +43 -0
  9. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/__init__.py +2 -2
  10. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/_base_module.py +78 -8
  11. digitalkin-0.2.17/src/digitalkin/modules/trigger_handler.py +47 -0
  12. digitalkin-0.2.17/src/digitalkin/utils/package_discover.py +358 -0
  13. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin.egg-info/PKG-INFO +1 -1
  14. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin.egg-info/SOURCES.txt +4 -2
  15. digitalkin-0.2.16/src/digitalkin/models/module/__init__.py +0 -12
  16. digitalkin-0.2.16/src/digitalkin/models/module/module_types.py +0 -11
  17. digitalkin-0.2.16/src/digitalkin/modules/trigger_module.py +0 -11
  18. {digitalkin-0.2.16 → digitalkin-0.2.17}/LICENSE +0 -0
  19. {digitalkin-0.2.16 → digitalkin-0.2.17}/README.md +0 -0
  20. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/__init__.py +0 -0
  21. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/mock/__init__.py +0 -0
  22. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/mock/mock_pb2.py +0 -0
  23. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
  24. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/server_async_insecure.py +0 -0
  25. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/server_async_secure.py +0 -0
  26. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/server_sync_insecure.py +0 -0
  27. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/base_server/server_sync_secure.py +0 -0
  28. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/modules/__init__.py +0 -0
  29. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/modules/cpu_intensive_module.py +0 -0
  30. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/modules/minimal_llm_module.py +0 -0
  31. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/modules/text_transform_module.py +0 -0
  32. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/services/filesystem_module.py +0 -0
  33. {digitalkin-0.2.16 → digitalkin-0.2.17}/examples/services/storage_module.py +0 -0
  34. {digitalkin-0.2.16 → digitalkin-0.2.17}/setup.cfg +0 -0
  35. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/__init__.py +0 -0
  36. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/__init__.py +0 -0
  37. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/_base_server.py +0 -0
  38. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/registry_server.py +0 -0
  39. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/registry_servicer.py +0 -0
  40. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/utils/exceptions.py +0 -0
  41. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/utils/factory.py +0 -0
  42. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +0 -0
  43. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/utils/models.py +0 -0
  44. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/grpc_servers/utils/types.py +0 -0
  45. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/logger.py +0 -0
  46. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/models/__init__.py +0 -0
  47. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/models/module/module.py +0 -0
  48. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/models/services/__init__.py +0 -0
  49. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/models/services/cost.py +0 -0
  50. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/models/services/storage.py +0 -0
  51. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/archetype_module.py +0 -0
  52. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/job_manager/base_job_manager.py +0 -0
  53. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/job_manager/job_manager_models.py +0 -0
  54. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/job_manager/single_job_manager.py +0 -0
  55. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/job_manager/taskiq_broker.py +0 -0
  56. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/job_manager/taskiq_job_manager.py +0 -0
  57. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/modules/tool_module.py +0 -0
  58. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/py.typed +0 -0
  59. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/__init__.py +0 -0
  60. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/agent/__init__.py +0 -0
  61. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/agent/agent_strategy.py +0 -0
  62. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/agent/default_agent.py +0 -0
  63. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/base_strategy.py +0 -0
  64. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/cost/__init__.py +0 -0
  65. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/cost/cost_strategy.py +0 -0
  66. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/cost/default_cost.py +0 -0
  67. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/cost/grpc_cost.py +0 -0
  68. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/filesystem/__init__.py +0 -0
  69. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
  70. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
  71. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
  72. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/identity/__init__.py +0 -0
  73. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/identity/default_identity.py +0 -0
  74. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/identity/identity_strategy.py +0 -0
  75. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/registry/__init__.py +0 -0
  76. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/registry/default_registry.py +0 -0
  77. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/registry/registry_strategy.py +0 -0
  78. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/services_config.py +0 -0
  79. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/services_models.py +0 -0
  80. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/setup/__init__.py +0 -0
  81. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/setup/default_setup.py +0 -0
  82. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/setup/grpc_setup.py +0 -0
  83. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/setup/setup_strategy.py +0 -0
  84. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/snapshot/__init__.py +0 -0
  85. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/snapshot/default_snapshot.py +0 -0
  86. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/snapshot/snapshot_strategy.py +0 -0
  87. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/storage/__init__.py +0 -0
  88. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/storage/default_storage.py +0 -0
  89. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/storage/grpc_storage.py +0 -0
  90. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/services/storage/storage_strategy.py +0 -0
  91. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/utils/__init__.py +0 -0
  92. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/utils/arg_parser.py +0 -0
  93. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/utils/development_mode_action.py +0 -0
  94. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin/utils/llm_ready_schema.py +0 -0
  95. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin.egg-info/dependency_links.txt +0 -0
  96. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin.egg-info/requires.txt +0 -0
  97. {digitalkin-0.2.16 → digitalkin-0.2.17}/src/digitalkin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.2.16
3
+ Version: 0.2.17
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -12,7 +12,7 @@
12
12
 
13
13
  keywords = [ "digitalkin", "kin", "agent", "gprc", "sdk" ]
14
14
  # Version of the package automatically updated by bump2version (that is why it is separated)
15
- version = "0.2.16"
15
+ version = "0.2.17"
16
16
 
17
17
  classifiers = [
18
18
  "Development Status :: 3 - Alpha",
@@ -212,7 +212,7 @@
212
212
  "ANN401",
213
213
  "PLR0912",
214
214
  "ANN201",
215
- "D100"
215
+ "D100",
216
216
  ]
217
217
 
218
218
  [tool.ruff.lint.pylint]
@@ -5,4 +5,4 @@ from importlib.metadata import PackageNotFoundError, version
5
5
  try:
6
6
  __version__ = version("digitalkin")
7
7
  except PackageNotFoundError:
8
- __version__ = "0.2.16"
8
+ __version__ = "0.2.17"
@@ -79,10 +79,10 @@ class ModuleServer(BaseServer):
79
79
 
80
80
  def start(self) -> None:
81
81
  """Start the module server and register with the registry if configured."""
82
- logger.critical(self.server_config)
82
+ logger.info(self.server_config)
83
83
  super().start()
84
84
 
85
- logger.critical(self.server_config)
85
+ logger.info(self.server_config)
86
86
  # If a registry address is provided, register the module
87
87
  if self.server_config.registry_address:
88
88
  try:
@@ -91,17 +91,17 @@ class ModuleServer(BaseServer):
91
91
  logger.exception("Failed to register with registry")
92
92
 
93
93
  if self.module_servicer is not None:
94
- logger.critical(
94
+ logger.info(
95
95
  "Setup post init started with config: %s", self.client_config
96
96
  )
97
97
  self.module_servicer.setup.__post_init__(self.client_config)
98
98
 
99
99
  async def start_async(self) -> None:
100
100
  """Start the module server and register with the registry if configured."""
101
- logger.critical(self.server_config)
101
+ logger.info(self.server_config)
102
102
  await super().start_async()
103
103
 
104
- logger.critical(self.server_config)
104
+ logger.info(self.server_config)
105
105
  # If a registry address is provided, register the module
106
106
  if self.server_config.registry_address:
107
107
  try:
@@ -110,7 +110,7 @@ class ModuleServer(BaseServer):
110
110
  logger.exception("Failed to register with registry")
111
111
 
112
112
  if self.module_servicer is not None:
113
- logger.critical(
113
+ logger.info(
114
114
  "Setup post init started with config: %s", self.client_config
115
115
  )
116
116
  await self.module_servicer.job_manager._start()
@@ -70,6 +70,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
70
70
  module_class: The module type to serve.
71
71
  """
72
72
  super().__init__()
73
+ module_class.discover()
73
74
  self.module_class = module_class
74
75
  job_manager_class = self.args.job_manager_mode.get_manager_class()
75
76
  self.job_manager = job_manager_class(module_class, self.args.services_mode)
@@ -0,0 +1,26 @@
1
+ """This module contains the models for the modules."""
2
+
3
+ from digitalkin.models.module.module import Module, ModuleStatus
4
+ from digitalkin.models.module.module_context import ModuleContext
5
+ from digitalkin.models.module.module_types import (
6
+ ConfigSetupModelT,
7
+ InputModel,
8
+ InputModelT,
9
+ InputTrigger,
10
+ OutputModelT,
11
+ SecretModelT,
12
+ SetupModelT,
13
+ )
14
+
15
+ __all__ = [
16
+ "ConfigSetupModelT",
17
+ "InputModel",
18
+ "InputModelT",
19
+ "InputTrigger",
20
+ "Module",
21
+ "ModuleContext",
22
+ "ModuleStatus",
23
+ "OutputModelT",
24
+ "SecretModelT",
25
+ "SetupModelT",
26
+ ]
@@ -0,0 +1,24 @@
1
+ """Define the module context used in the triggers."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ from digitalkin.services.cost.cost_strategy import CostStrategy
6
+ from digitalkin.services.filesystem.filesystem_strategy import FilesystemStrategy
7
+ from digitalkin.services.storage.storage_strategy import StorageStrategy
8
+
9
+
10
+ class ModuleContext(SimpleNamespace):
11
+ """ModuleContext provides a container for strategies and resources used by a module.
12
+
13
+ Attributes:
14
+ cost (CostStrategy): The strategy used to calculate or manage costs within the module.
15
+ filesystem (FilesystemStrategy): The strategy for interacting with the filesystem.
16
+ storage (StorageStrategy): The strategy for handling storage operations.
17
+
18
+ This context object is designed to be passed to module components, providing them with
19
+ access to shared strategies and resources. Additional attributes may be set dynamically.
20
+ """
21
+
22
+ cost: CostStrategy
23
+ filesystem: FilesystemStrategy
24
+ storage: StorageStrategy
@@ -0,0 +1,43 @@
1
+ """Types for module models."""
2
+
3
+ from typing import TypeVar
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class InputTrigger(BaseModel):
9
+ """Defines the root input model exposing the protocol.
10
+
11
+ The mandatory protocol is important to define the module beahvior following the user or agent input.
12
+
13
+ Example:
14
+ class MyInput(InputModel):
15
+ root: InputTrigger
16
+ user_define_data: Any
17
+
18
+ # Usage
19
+ my_input = MyInput(root=InputTrigger(protocol="message"))
20
+ print(my_input.root.protocol) # Output: message
21
+ """
22
+
23
+ protocol: str
24
+
25
+
26
+ class InputModel(BaseModel):
27
+ """Base definition of input model showing mandatory root fields.
28
+
29
+ The Model define the Module Input, usually referring to multiple input type defined by an union.
30
+
31
+ Example:
32
+ class ModuleInput(InputModel):
33
+ root: FileInput | MessageInput
34
+ """
35
+
36
+ root: InputTrigger
37
+
38
+
39
+ ConfigSetupModelT = TypeVar("ConfigSetupModelT", bound=BaseModel | None)
40
+ InputModelT = TypeVar("InputModelT", bound=InputModel)
41
+ OutputModelT = TypeVar("OutputModelT", bound=BaseModel)
42
+ SetupModelT = TypeVar("SetupModelT", bound=BaseModel)
43
+ SecretModelT = TypeVar("SecretModelT", bound=BaseModel)
@@ -2,6 +2,6 @@
2
2
 
3
3
  from digitalkin.modules.archetype_module import ArchetypeModule
4
4
  from digitalkin.modules.tool_module import ToolModule
5
- from digitalkin.modules.trigger_module import TriggerModule
5
+ from digitalkin.modules.trigger_handler import TriggerHandler
6
6
 
7
- __all__ = ["ArchetypeModule", "ToolModule", "TriggerModule"]
7
+ __all__ = ["ArchetypeModule", "ToolModule", "TriggerHandler"]
@@ -19,6 +19,8 @@ from digitalkin.models.module import (
19
19
  SecretModelT,
20
20
  SetupModelT,
21
21
  )
22
+ from digitalkin.models.module.module_context import ModuleContext
23
+ from digitalkin.modules.trigger_handler import TriggerHandler
22
24
  from digitalkin.services.agent.agent_strategy import AgentStrategy
23
25
  from digitalkin.services.cost.cost_strategy import CostStrategy
24
26
  from digitalkin.services.filesystem.filesystem_strategy import FilesystemStrategy
@@ -28,17 +30,18 @@ from digitalkin.services.services_config import ServicesConfig, ServicesStrategy
28
30
  from digitalkin.services.snapshot.snapshot_strategy import SnapshotStrategy
29
31
  from digitalkin.services.storage.storage_strategy import StorageStrategy
30
32
  from digitalkin.utils.llm_ready_schema import llm_ready_schema
33
+ from digitalkin.utils.package_discover import ModuleDiscoverer
31
34
 
32
35
 
33
36
  class ModuleErrorModel(BaseModel):
34
- """Typed error/code model."""
37
+ """typed error/code model."""
35
38
 
36
39
  code: str
37
40
  exception: str
38
41
  short_description: str
39
42
 
40
43
 
41
- class BaseModule(
44
+ class BaseModule( # noqa: PLR0904
42
45
  ABC,
43
46
  Generic[
44
47
  InputModelT,
@@ -60,6 +63,9 @@ class BaseModule(
60
63
  secret_format: type[SecretModelT]
61
64
  metadata: ClassVar[dict[str, Any]]
62
65
 
66
+ context: ModuleContext
67
+ triggers_discoverer: ClassVar[ModuleDiscoverer]
68
+
63
69
  # service config params
64
70
  services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]]
65
71
  services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]]
@@ -95,6 +101,13 @@ class BaseModule(
95
101
  # Initialize services configuration
96
102
  self._init_strategies()
97
103
 
104
+ # Initialize minimum context
105
+ self.context = ModuleContext(
106
+ storage=self.storage,
107
+ cost=self.cost,
108
+ filesystem=self.filesystem,
109
+ )
110
+
98
111
  @property
99
112
  def status(self) -> ModuleStatus:
100
113
  """Get the module status.
@@ -165,10 +178,12 @@ class BaseModule(
165
178
  Returns:
166
179
  The JSON schema of the config setup format as a string.
167
180
  """
168
- if cls.config_setup_format is not None:
181
+ config_setup_format = getattr(cls, "config_setup_format", None)
182
+
183
+ if config_setup_format is not None:
169
184
  if llm_format:
170
- return json.dumps(llm_ready_schema(cls.config_setup_format), indent=2)
171
- return json.dumps(cls.config_setup_format.model_json_schema(), indent=2)
185
+ return json.dumps(llm_ready_schema(config_setup_format), indent=2)
186
+ return json.dumps(config_setup_format.model_json_schema(), indent=2)
172
187
  msg = "'%s' class does not define an 'config_setup_format'."
173
188
  raise OptionalFeatureNotImplementedError(msg)
174
189
 
@@ -249,6 +264,32 @@ class BaseModule(
249
264
  """
250
265
  return cls.output_format(**output_data)
251
266
 
267
+ @classmethod
268
+ def discover(cls) -> None:
269
+ """Discover and register all TriggerHandler subclasses in the specified package or current directory.
270
+
271
+ Dynamically import all Python modules in the specified package or current directory,
272
+ triggering class registrations for subclasses of TriggerHandler whose names end with 'Trigger'.
273
+
274
+ If a package is provided, all .py files within its path are imported; otherwise, the current
275
+ working directory is searched. For each imported module, any class matching the criteria is
276
+ registered via cls.register(). Errors during import are logged at debug level.
277
+ """
278
+ cls.triggers_discoverer.discover_modules()
279
+ logger.debug("discovered: %s", cls.triggers_discoverer)
280
+
281
+ @classmethod
282
+ def register(cls, handler_cls: type[TriggerHandler]) -> type[TriggerHandler]:
283
+ """Dynamically register the trigger class.
284
+
285
+ Args:
286
+ handler_cls: type of the trigger handler to register.
287
+
288
+ Returns:
289
+ type of the trigger handler.
290
+ """
291
+ return cls.triggers_discoverer.register_trigger(handler_cls)
292
+
252
293
  @abstractmethod
253
294
  async def run_config_setup(
254
295
  self,
@@ -269,15 +310,42 @@ class BaseModule(
269
310
  """Initialize the module."""
270
311
  raise NotImplementedError
271
312
 
272
- @abstractmethod
273
313
  async def run(
274
314
  self,
275
315
  input_data: InputModelT,
276
316
  setup_data: SetupModelT,
277
317
  callback: Callable[[OutputModelT], Coroutine[Any, Any, None]],
278
318
  ) -> None:
279
- """Run the module."""
280
- raise NotImplementedError
319
+ """Run the module with the given input and setup data.
320
+
321
+ This method validates the input data, determines the protocol from the input,
322
+ and dispatches the request to the corresponding trigger handler. The trigger handler
323
+ is responsible for processing the input and invoking the callback with the result.
324
+
325
+ Triggers:
326
+ - The method is triggered when a module run is requested with specific input and setup data.
327
+ - The protocol specified in the input determines which trigger handler is invoked.
328
+
329
+ Args:
330
+ input_data (InputModelT): The input data to be processed by the module.
331
+ setup_data (SetupModelT): The setup or configuration data required for the module.
332
+ callback (Callable[[OutputModelT], Coroutine[Any, Any, None]]): callback to be invoked to stream any result.
333
+
334
+ Raises:
335
+ ValueError: If no handler for the protocol is found.
336
+ """
337
+ input_instance = self.input_format.model_validate(input_data)
338
+ handler_instance = self.triggers_discoverer.get_trigger(
339
+ input_instance.root.protocol,
340
+ input_instance.root,
341
+ )
342
+
343
+ await handler_instance.handle(
344
+ input_instance.root,
345
+ setup_data,
346
+ callback,
347
+ self.context,
348
+ )
281
349
 
282
350
  @abstractmethod
283
351
  async def cleanup(self) -> None:
@@ -338,6 +406,8 @@ class BaseModule(
338
406
  return
339
407
 
340
408
  try:
409
+ logger.info("Init the discod input handlers.")
410
+ self.triggers_discoverer.init_handlers(self.context)
341
411
  logger.info("Run lifecycle")
342
412
  self._status = ModuleStatus.RUNNING
343
413
  self._task = asyncio.create_task(
@@ -0,0 +1,47 @@
1
+ """Definition of the Trigger type."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable, Coroutine
5
+ from typing import Any, ClassVar, Generic
6
+
7
+ from digitalkin.models.module.module_types import InputModelT, OutputModelT, SetupModelT
8
+ from digitalkin.modules._base_module import ModuleContext
9
+
10
+
11
+ class TriggerHandler(ABC, Generic[InputModelT, SetupModelT, OutputModelT]):
12
+ """Base class for all input-trigger handlers.
13
+
14
+ Each handler declares:
15
+ - protocol_key: the Literal value this handler processes
16
+ - handle(): logic to process the validated payload
17
+ """
18
+
19
+ protocol: ClassVar[str]
20
+ input_format: type[InputModelT]
21
+ output_format: type[OutputModelT]
22
+
23
+ def __init__(self, context: ModuleContext) -> None:
24
+ """Initialize the TriggerHandler with the given context."""
25
+
26
+ @abstractmethod
27
+ async def handle(
28
+ self,
29
+ input_data: InputModelT,
30
+ setup_data: SetupModelT,
31
+ callback: Callable[[Any], Coroutine[Any, Any, None]],
32
+ context: ModuleContext,
33
+ ) -> None:
34
+ """Asynchronously processes the input data specific to Handler and streams results via the provided callback.
35
+
36
+ Args:
37
+ input_data (InputModelT): The input data to be processed by the handler.
38
+ setup_data (SetupModelT): The setup or configuration data required for processing.
39
+ callback (Callable[[Any], Coroutine[Any, Any, None]]): callback that stream results.
40
+ context (ModuleContext): The context object containing module-specific information and resources.
41
+
42
+ Returns:
43
+ Any: The result of the processing, if applicable.
44
+
45
+ Note:
46
+ The callback must be awaited to ensure results are streamed correctly during processing.
47
+ """
@@ -0,0 +1,358 @@
1
+ """."""
2
+
3
+ import importlib
4
+ import importlib.util
5
+ import logging
6
+ import pkgutil
7
+ import sys
8
+ from fnmatch import fnmatch
9
+ from pathlib import Path
10
+ from typing import ClassVar
11
+
12
+ from digitalkin.models.module.module_context import ModuleContext
13
+ from digitalkin.models.module.module_types import InputTrigger
14
+ from digitalkin.modules.trigger_handler import TriggerHandler
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SecurityError(Exception):
20
+ """Raised when security constraints are violated."""
21
+
22
+
23
+ class DiscoveryError(Exception):
24
+ """Raised when discovery fails due to invalid inputs."""
25
+
26
+
27
+ class ModuleDiscoverer:
28
+ """Encapsulates secure, structured discovery and import of trigger modules.
29
+
30
+ Attributes:
31
+ packages: List of Python package paths to scan.
32
+ file_pattern: Glob pattern to match module filenames.
33
+ safe_mode: If True, skips unsafe imports.
34
+ max_file_size: Maximum file size allowed for import (bytes).
35
+ """
36
+
37
+ FORBIDDEN_MODULE_PATTERNS: ClassVar[set[str]] = {
38
+ "__pycache__",
39
+ ".pyc",
40
+ ".pyo",
41
+ ".pyd",
42
+ "test_",
43
+ "_test",
44
+ "conftest",
45
+ }
46
+
47
+ trigger_handlers: ClassVar[dict[str, tuple[TriggerHandler, ...]]] = {}
48
+ _trigger_handlers_cls: ClassVar[dict[str, list[type[TriggerHandler]]]] = {}
49
+
50
+ def _validate_inputs(self) -> None:
51
+ """Validate initial discovery inputs.
52
+
53
+ Raises:
54
+ DiscoveryError: If packages list is invalid.
55
+ SecurityError: If file pattern or package names are unsafe.
56
+ """
57
+ if not self.packages or not isinstance(self.packages, list):
58
+ msg = "Packages must be a non-empty list"
59
+ raise DiscoveryError(msg)
60
+ self._validate_file_pattern()
61
+ for pkg in self.packages:
62
+ self._validate_package_name(pkg)
63
+
64
+ def _discover_package(self, package_name: str) -> dict[str, bool]:
65
+ """Import a package and scan its __path__ for modules.
66
+
67
+ Args:
68
+ package_name: Dotted path of the package to scan.
69
+
70
+ Returns:
71
+ Mapping of module names to import status.
72
+ """
73
+ try:
74
+ pkg = importlib.import_module(package_name)
75
+ except ImportError:
76
+ logger.exception("Could not import package %s", package_name)
77
+ return {}
78
+
79
+ paths = getattr(pkg, "__path__", None)
80
+ if not paths:
81
+ logger.warning("Package %s has no __path__", package_name)
82
+ return {}
83
+
84
+ results: dict[str, bool] = {}
85
+ for path_str in paths:
86
+ base_path = Path(path_str).resolve()
87
+ results.update(self._discover_in_path(package_name, base_path))
88
+ return results
89
+
90
+ def _discover_in_path(self, package_name: str, base_path: Path) -> dict[str, bool]:
91
+ """Walk a filesystem path to locate and process modules.
92
+
93
+ Args:
94
+ package_name: Root package name for prefixing.
95
+ base_path: Filesystem path of the package.
96
+
97
+ Returns:
98
+ Mapping of module names to import status.
99
+ """
100
+ results: dict[str, bool] = {}
101
+ if not base_path.is_dir():
102
+ logger.warning("Invalid package path: %s", base_path)
103
+ return results
104
+
105
+ for _, module_name, is_pkg in pkgutil.walk_packages(
106
+ [str(base_path)], prefix=f"{package_name}.", onerror=lambda e: logger.error("Walk error: %s", e)
107
+ ):
108
+ if is_pkg or module_name in results:
109
+ continue
110
+ results[module_name] = self._process_module(module_name, base_path, package_name)
111
+ return results
112
+
113
+ def _process_module(self, module_name: str, base_path: Path, package_name: str) -> bool:
114
+ """Validate module file, import it, and validate the trigger class.
115
+
116
+ Args:
117
+ module_name: Full dotted module path.
118
+ base_path: Filesystem base path of the package.
119
+ package_name: Root package for path resolution.
120
+
121
+ Returns:
122
+ True if import and validation succeed, False otherwise.
123
+ """
124
+ try:
125
+ module_file = self._module_file_path(module_name, base_path, package_name)
126
+ self._validate_module_path(module_file, base_path)
127
+ if not fnmatch(module_file.name, self.file_pattern):
128
+ return False
129
+ if not self._is_safe_module_name(module_name):
130
+ logger.debug("Skipping unsafe module: %s", module_name)
131
+ return False
132
+ if not self._safe_import_module(module_name, module_file):
133
+ return False
134
+
135
+ except SecurityError:
136
+ logger.exception("Security violation %s", module_name)
137
+ return False
138
+ except Exception:
139
+ logger.exception("Error processing %s", module_name)
140
+ return False
141
+ return True
142
+
143
+ @staticmethod
144
+ def _module_file_path(module_name: str, base_path: Path, package_name: str) -> Path:
145
+ """Compute filesystem Path for a module's .py file.
146
+
147
+ Args:
148
+ module_name: Full module name.
149
+ base_path: Base directory of the package.
150
+ package_name: Root package prefix.
151
+
152
+ Returns:
153
+ Path to the module's .py file.
154
+ """
155
+ rel = module_name.replace(f"{package_name}.", "").replace(".", "/")
156
+ return base_path / f"{rel}.py"
157
+
158
+ @staticmethod
159
+ def _validate_package_name(package_name: str) -> None:
160
+ """Validate that a package name is safe and well-formed.
161
+
162
+ Args:
163
+ package_name: Dotted Python package name.
164
+
165
+ Raises:
166
+ SecurityError: On invalid package names.
167
+ """
168
+ if not package_name or not isinstance(package_name, str):
169
+ msg = "Package name must be a non-empty string"
170
+ raise SecurityError(msg)
171
+ if any(part in package_name for part in ("..", "/", "\\", "\x00")):
172
+ msg = "Invalid package name: %s"
173
+ raise SecurityError(msg, package_name)
174
+ if not all(part.isidentifier() for part in package_name.split(".")):
175
+ msg = "Invalid Python package name: %s"
176
+ raise SecurityError(msg, package_name)
177
+
178
+ def _validate_file_pattern(self) -> None:
179
+ """Validate that the file glob pattern is safe.
180
+
181
+ Raises:
182
+ SecurityError: On dangerous patterns.
183
+ """
184
+ pattern = self.file_pattern
185
+ if not pattern or not isinstance(pattern, str):
186
+ msg = "File pattern must be a non-empty string"
187
+ raise SecurityError(msg)
188
+ if any(d in pattern for d in ("..", "/", "\\", "\x00", "**/")):
189
+ msg = "Dangerous pattern detected: %s"
190
+ raise SecurityError(msg, pattern)
191
+ if not pattern.endswith(".py"):
192
+ msg = "Pattern must target Python files (.py)"
193
+ raise SecurityError(msg)
194
+
195
+ def _validate_module_path(self, module_path: Path, base_path: Path) -> None:
196
+ """Ensure module_path resides under base_path and is within size limits.
197
+
198
+ Args:
199
+ module_path: Path to the module file.
200
+ base_path: Root directory for the package.
201
+
202
+ Raises:
203
+ SecurityError: On invalid paths or oversize files.
204
+ """
205
+ try:
206
+ resolved_module = module_path.resolve()
207
+ resolved_base = base_path.resolve()
208
+ if not str(resolved_module).startswith(str(resolved_base)):
209
+ msg = "Path traversal attempt: %s"
210
+ raise SecurityError(msg, module_path)
211
+ if not resolved_module.exists() or not resolved_module.is_file():
212
+ msg = "Invalid module path: %s"
213
+ raise SecurityError(msg, module_path)
214
+ if resolved_module.stat().st_size > self.max_file_size:
215
+ msg = "Module file too large: %s"
216
+ raise SecurityError(msg, module_path)
217
+ except (OSError, ValueError) as e:
218
+ msg = "Invalid module path: %s"
219
+ raise SecurityError(msg, module_path) from e
220
+
221
+ def _is_safe_module_name(self, module_name: str) -> bool:
222
+ """Check module name against forbidden patterns.
223
+
224
+ Args:
225
+ module_name: Full dotted module name.
226
+
227
+ Returns:
228
+ True if safe, False otherwise.
229
+ """
230
+ if not module_name or not all(part.isidentifier() for part in module_name.split(".")):
231
+ return False
232
+ return not any(p in module_name for p in self.FORBIDDEN_MODULE_PATTERNS)
233
+
234
+ def _safe_import_module(self, module_name: str, module_path: Path) -> bool:
235
+ """Import a module by spec and execute it.
236
+
237
+ Args:
238
+ module_name: Dotted module name.
239
+ module_path: Filesystem path to .py file.
240
+
241
+ Returns:
242
+ True if imported successfully, False otherwise.
243
+ """
244
+ try:
245
+ if not self._is_safe_module_name(module_name):
246
+ return False
247
+ if module_name in sys.modules:
248
+ logger.debug("Module %s already imported", module_name)
249
+ return True
250
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
251
+ if spec is None or spec.loader is None:
252
+ logger.error("Could not create valid spec for %s", module_name)
253
+ return False
254
+ module = importlib.util.module_from_spec(spec)
255
+ sys.modules[module_name] = module
256
+ spec.loader.exec_module(module)
257
+ logger.debug("Successfully imported %s", module_name)
258
+ except Exception:
259
+ sys.modules.pop(module_name, None)
260
+ logger.exception("Failed to import %s", module_name)
261
+ return False
262
+ return True
263
+
264
+ def __str__(self) -> str:
265
+ """Return a string representation of registered trigger handler classes."""
266
+ return str(self._trigger_handlers_cls)
267
+
268
+ def __init__(
269
+ self,
270
+ packages: list[str],
271
+ file_pattern: str = "*_trigger.py",
272
+ max_file_size: int = 1024 * 1024, # 1Mb
273
+ ) -> None:
274
+ """Initialize the discoverer.
275
+
276
+ Args:
277
+ packages: List of package names to scan.
278
+ file_pattern: Glob pattern for matching modules.
279
+ safe_mode: If True, blocks modules with forbidden names.
280
+ max_file_size: Limit for module file sizes in bytes.
281
+ """
282
+ self.packages = packages
283
+ self.file_pattern = file_pattern
284
+ self.max_file_size = max_file_size
285
+
286
+ def discover_modules(self) -> dict[str, bool]:
287
+ """Discover and import matching modules across configured packages.
288
+
289
+ Raises:
290
+ DiscoveryError: If initial inputs are invalid.
291
+
292
+ Returns:
293
+ results infos
294
+ """
295
+ self._validate_inputs()
296
+ results: dict[str, bool] = {}
297
+ for pkg in self.packages:
298
+ results.update(self._discover_package(pkg))
299
+ return results
300
+
301
+ def register_trigger(self, handler_cls: type[TriggerHandler]) -> type[TriggerHandler]:
302
+ """Register a trigger handler class for a specific protocol.
303
+
304
+ Args:
305
+ handler_cls: The trigger handler class to register.
306
+
307
+ Returns:
308
+ The registered trigger handler class.
309
+
310
+ Raises:
311
+ ValueError: If a handler for the protocol is already registered.
312
+ """
313
+ key = handler_cls.protocol
314
+ if key not in self._trigger_handlers_cls:
315
+ self._trigger_handlers_cls[key] = []
316
+
317
+ self._trigger_handlers_cls[key].append(handler_cls)
318
+ return handler_cls
319
+
320
+ def init_handlers(self, context: ModuleContext) -> None:
321
+ """Initialize all registered trigger handler instances.
322
+
323
+ This method iterates over all registered trigger handler classes in
324
+ `_trigger_handlers_cls`, instantiates each handler with the current module
325
+ context, and stores the instance in `_trigger_handlers`.
326
+ This allows the module to dispatch incoming protocol requests
327
+ to the correct handler instance at runtime while keeping a shared context.
328
+ """
329
+ for protocol, handlers_cls in self._trigger_handlers_cls.items():
330
+ self.trigger_handlers[protocol] = tuple(handler_cls(context) for handler_cls in set(handlers_cls))
331
+
332
+ def get_trigger(self, protocol: str, input_instance: InputTrigger) -> TriggerHandler:
333
+ """Retrieve a trigger handler instance based on the provided protocol and input instance type.
334
+
335
+ Args:
336
+ protocol: The protocol name (ignored internally, `input_instance.protocol` is used instead).
337
+ input_instance: The input trigger instance used to determine the correct handler.
338
+
339
+ Returns:
340
+ TriggerHandler: An instance of the trigger handler matching the input format.
341
+
342
+ Raises:
343
+ ValueError: If no handler is registered for the specified protocol,
344
+ or if no handler matches the type of the input instance.
345
+ """
346
+ logger.debug("Trigger type invoked: %s", input_instance)
347
+ protocol = input_instance.protocol
348
+
349
+ if (protocols := self.trigger_handlers.get(protocol)) is None:
350
+ msg = f"No handler for protocol '{protocol}'"
351
+ raise ValueError(msg)
352
+
353
+ try:
354
+ handler_instance = next(x for x in protocols if isinstance(input_instance, x.input_format))
355
+ except Exception:
356
+ msg = f"No handler for input format '{type(input_instance)=}'"
357
+ raise ValueError(msg)
358
+ return handler_instance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.2.16
3
+ Version: 0.2.17
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -38,6 +38,7 @@ src/digitalkin/grpc_servers/utils/types.py
38
38
  src/digitalkin/models/__init__.py
39
39
  src/digitalkin/models/module/__init__.py
40
40
  src/digitalkin/models/module/module.py
41
+ src/digitalkin/models/module/module_context.py
41
42
  src/digitalkin/models/module/module_types.py
42
43
  src/digitalkin/models/services/__init__.py
43
44
  src/digitalkin/models/services/cost.py
@@ -46,7 +47,7 @@ src/digitalkin/modules/__init__.py
46
47
  src/digitalkin/modules/_base_module.py
47
48
  src/digitalkin/modules/archetype_module.py
48
49
  src/digitalkin/modules/tool_module.py
49
- src/digitalkin/modules/trigger_module.py
50
+ src/digitalkin/modules/trigger_handler.py
50
51
  src/digitalkin/modules/job_manager/base_job_manager.py
51
52
  src/digitalkin/modules/job_manager/job_manager_models.py
52
53
  src/digitalkin/modules/job_manager/single_job_manager.py
@@ -87,4 +88,5 @@ src/digitalkin/services/storage/storage_strategy.py
87
88
  src/digitalkin/utils/__init__.py
88
89
  src/digitalkin/utils/arg_parser.py
89
90
  src/digitalkin/utils/development_mode_action.py
90
- src/digitalkin/utils/llm_ready_schema.py
91
+ src/digitalkin/utils/llm_ready_schema.py
92
+ src/digitalkin/utils/package_discover.py
@@ -1,12 +0,0 @@
1
- """This module contains the models for the modules."""
2
-
3
- from digitalkin.models.module.module import Module, ModuleStatus
4
- from digitalkin.models.module.module_types import (
5
- ConfigSetupModelT,
6
- InputModelT,
7
- OutputModelT,
8
- SecretModelT,
9
- SetupModelT,
10
- )
11
-
12
- __all__ = ["ConfigSetupModelT", "InputModelT", "Module", "ModuleStatus", "OutputModelT", "SecretModelT", "SetupModelT"]
@@ -1,11 +0,0 @@
1
- """Types for module models."""
2
-
3
- from typing import TypeVar
4
-
5
- from pydantic import BaseModel
6
-
7
- ConfigSetupModelT = TypeVar("ConfigSetupModelT", bound=BaseModel | None)
8
- InputModelT = TypeVar("InputModelT", bound=BaseModel)
9
- OutputModelT = TypeVar("OutputModelT", bound=BaseModel)
10
- SetupModelT = TypeVar("SetupModelT", bound=BaseModel)
11
- SecretModelT = TypeVar("SecretModelT", bound=BaseModel)
@@ -1,11 +0,0 @@
1
- """TriggerModule extends BaseModule to implement specific module types."""
2
-
3
- from abc import ABC
4
-
5
- from digitalkin.models.module.module_types import ConfigSetupModelT
6
- from digitalkin.modules._base_module import BaseModule, InputModelT, OutputModelT, SecretModelT, SetupModelT
7
-
8
-
9
- class TriggerModule(BaseModule[InputModelT, OutputModelT, SetupModelT, SecretModelT,
10
- ConfigSetupModelT,], ABC):
11
- """TriggerModule extends BaseModule to implement specific module types."""
File without changes
File without changes
File without changes