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,357 @@
1
+ """Secure module discovery and import utility for trigger handlers."""
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 DataTrigger
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
+ max_file_size: Limit for module file sizes in bytes.
280
+ """
281
+ self.packages = packages
282
+ self.file_pattern = file_pattern
283
+ self.max_file_size = max_file_size
284
+
285
+ def discover_modules(self) -> dict[str, bool]:
286
+ """Discover and import matching modules across configured packages.
287
+
288
+ Raises:
289
+ DiscoveryError: If initial inputs are invalid.
290
+
291
+ Returns:
292
+ results infos
293
+ """
294
+ self._validate_inputs()
295
+ results: dict[str, bool] = {}
296
+ for pkg in self.packages:
297
+ results.update(self._discover_package(pkg))
298
+ return results
299
+
300
+ def register_trigger(self, handler_cls: type[TriggerHandler]) -> type[TriggerHandler]:
301
+ """Register a trigger handler class for a specific protocol.
302
+
303
+ Args:
304
+ handler_cls: The trigger handler class to register.
305
+
306
+ Returns:
307
+ The registered trigger handler class.
308
+
309
+ Raises:
310
+ ValueError: If a handler for the protocol is already registered.
311
+ """
312
+ key = handler_cls.protocol
313
+ if key not in self._trigger_handlers_cls:
314
+ self._trigger_handlers_cls[key] = []
315
+
316
+ self._trigger_handlers_cls[key].append(handler_cls)
317
+ return handler_cls
318
+
319
+ def init_handlers(self, context: ModuleContext) -> None:
320
+ """Initialize all registered trigger handler instances.
321
+
322
+ This method iterates over all registered trigger handler classes in
323
+ `_trigger_handlers_cls`, instantiates each handler with the current module
324
+ context, and stores the instance in `_trigger_handlers`.
325
+ This allows the module to dispatch incoming protocol requests
326
+ to the correct handler instance at runtime while keeping a shared context.
327
+ """
328
+ for protocol, handlers_cls in self._trigger_handlers_cls.items():
329
+ self.trigger_handlers[protocol] = tuple(handler_cls(context) for handler_cls in set(handlers_cls))
330
+
331
+ def get_trigger(self, protocol: str, input_instance: DataTrigger) -> TriggerHandler:
332
+ """Retrieve a trigger handler instance based on the provided protocol and input instance type.
333
+
334
+ Args:
335
+ protocol: The protocol name (ignored internally, `input_instance.protocol` is used instead).
336
+ input_instance: The input trigger instance used to determine the correct handler.
337
+
338
+ Returns:
339
+ TriggerHandler: An instance of the trigger handler matching the input format.
340
+
341
+ Raises:
342
+ ValueError: If no handler is registered for the specified protocol,
343
+ or if no handler matches the type of the input instance.
344
+ """
345
+ logger.debug("Trigger type invoked: %s", input_instance)
346
+ protocol = input_instance.protocol
347
+
348
+ if (protocols := self.trigger_handlers.get(protocol)) is None:
349
+ msg = f"No handler for protocol '{protocol}'"
350
+ raise ValueError(msg)
351
+
352
+ try:
353
+ handler_instance = next(x for x in protocols if isinstance(input_instance, x.input_format))
354
+ except Exception:
355
+ msg = f"No handler for input format '{type(input_instance)=}'"
356
+ raise ValueError(msg)
357
+ return handler_instance