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.
- base_server/__init__.py +1 -0
- base_server/mock/__init__.py +5 -0
- base_server/mock/mock_pb2.py +39 -0
- base_server/mock/mock_pb2_grpc.py +102 -0
- base_server/server_async_insecure.py +125 -0
- base_server/server_async_secure.py +143 -0
- base_server/server_sync_insecure.py +103 -0
- base_server/server_sync_secure.py +122 -0
- digitalkin/__init__.py +8 -0
- digitalkin/__version__.py +8 -0
- digitalkin/core/__init__.py +1 -0
- digitalkin/core/common/__init__.py +9 -0
- digitalkin/core/common/factories.py +156 -0
- digitalkin/core/job_manager/__init__.py +1 -0
- digitalkin/core/job_manager/base_job_manager.py +288 -0
- digitalkin/core/job_manager/single_job_manager.py +354 -0
- digitalkin/core/job_manager/taskiq_broker.py +311 -0
- digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
- digitalkin/core/task_manager/__init__.py +1 -0
- digitalkin/core/task_manager/base_task_manager.py +539 -0
- digitalkin/core/task_manager/local_task_manager.py +108 -0
- digitalkin/core/task_manager/remote_task_manager.py +87 -0
- digitalkin/core/task_manager/surrealdb_repository.py +266 -0
- digitalkin/core/task_manager/task_executor.py +249 -0
- digitalkin/core/task_manager/task_session.py +406 -0
- digitalkin/grpc_servers/__init__.py +1 -0
- digitalkin/grpc_servers/_base_server.py +486 -0
- digitalkin/grpc_servers/module_server.py +208 -0
- digitalkin/grpc_servers/module_servicer.py +516 -0
- digitalkin/grpc_servers/utils/__init__.py +1 -0
- digitalkin/grpc_servers/utils/exceptions.py +29 -0
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
- digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
- digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
- digitalkin/logger.py +157 -0
- digitalkin/mixins/__init__.py +19 -0
- digitalkin/mixins/base_mixin.py +10 -0
- digitalkin/mixins/callback_mixin.py +24 -0
- digitalkin/mixins/chat_history_mixin.py +110 -0
- digitalkin/mixins/cost_mixin.py +76 -0
- digitalkin/mixins/file_history_mixin.py +93 -0
- digitalkin/mixins/filesystem_mixin.py +46 -0
- digitalkin/mixins/logger_mixin.py +51 -0
- digitalkin/mixins/storage_mixin.py +79 -0
- digitalkin/models/__init__.py +8 -0
- digitalkin/models/core/__init__.py +1 -0
- digitalkin/models/core/job_manager_models.py +36 -0
- digitalkin/models/core/task_monitor.py +70 -0
- digitalkin/models/grpc_servers/__init__.py +1 -0
- digitalkin/models/grpc_servers/models.py +275 -0
- digitalkin/models/grpc_servers/types.py +24 -0
- digitalkin/models/module/__init__.py +25 -0
- digitalkin/models/module/module.py +40 -0
- digitalkin/models/module/module_context.py +149 -0
- digitalkin/models/module/module_types.py +393 -0
- digitalkin/models/module/utility.py +146 -0
- digitalkin/models/services/__init__.py +10 -0
- digitalkin/models/services/cost.py +54 -0
- digitalkin/models/services/registry.py +42 -0
- digitalkin/models/services/storage.py +44 -0
- digitalkin/modules/__init__.py +11 -0
- digitalkin/modules/_base_module.py +517 -0
- digitalkin/modules/archetype_module.py +23 -0
- digitalkin/modules/tool_module.py +23 -0
- digitalkin/modules/trigger_handler.py +48 -0
- digitalkin/modules/triggers/__init__.py +12 -0
- digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
- digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
- digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
- digitalkin/py.typed +0 -0
- digitalkin/services/__init__.py +30 -0
- digitalkin/services/agent/__init__.py +6 -0
- digitalkin/services/agent/agent_strategy.py +19 -0
- digitalkin/services/agent/default_agent.py +13 -0
- digitalkin/services/base_strategy.py +22 -0
- digitalkin/services/communication/__init__.py +7 -0
- digitalkin/services/communication/communication_strategy.py +76 -0
- digitalkin/services/communication/default_communication.py +101 -0
- digitalkin/services/communication/grpc_communication.py +223 -0
- digitalkin/services/cost/__init__.py +14 -0
- digitalkin/services/cost/cost_strategy.py +100 -0
- digitalkin/services/cost/default_cost.py +114 -0
- digitalkin/services/cost/grpc_cost.py +138 -0
- digitalkin/services/filesystem/__init__.py +7 -0
- digitalkin/services/filesystem/default_filesystem.py +417 -0
- digitalkin/services/filesystem/filesystem_strategy.py +252 -0
- digitalkin/services/filesystem/grpc_filesystem.py +317 -0
- digitalkin/services/identity/__init__.py +6 -0
- digitalkin/services/identity/default_identity.py +15 -0
- digitalkin/services/identity/identity_strategy.py +14 -0
- digitalkin/services/registry/__init__.py +27 -0
- digitalkin/services/registry/default_registry.py +141 -0
- digitalkin/services/registry/exceptions.py +47 -0
- digitalkin/services/registry/grpc_registry.py +306 -0
- digitalkin/services/registry/registry_models.py +43 -0
- digitalkin/services/registry/registry_strategy.py +98 -0
- digitalkin/services/services_config.py +200 -0
- digitalkin/services/services_models.py +65 -0
- digitalkin/services/setup/__init__.py +1 -0
- digitalkin/services/setup/default_setup.py +219 -0
- digitalkin/services/setup/grpc_setup.py +343 -0
- digitalkin/services/setup/setup_strategy.py +145 -0
- digitalkin/services/snapshot/__init__.py +6 -0
- digitalkin/services/snapshot/default_snapshot.py +39 -0
- digitalkin/services/snapshot/snapshot_strategy.py +30 -0
- digitalkin/services/storage/__init__.py +7 -0
- digitalkin/services/storage/default_storage.py +228 -0
- digitalkin/services/storage/grpc_storage.py +214 -0
- digitalkin/services/storage/storage_strategy.py +273 -0
- digitalkin/services/user_profile/__init__.py +12 -0
- digitalkin/services/user_profile/default_user_profile.py +55 -0
- digitalkin/services/user_profile/grpc_user_profile.py +69 -0
- digitalkin/services/user_profile/user_profile_strategy.py +40 -0
- digitalkin/utils/__init__.py +29 -0
- digitalkin/utils/arg_parser.py +92 -0
- digitalkin/utils/development_mode_action.py +51 -0
- digitalkin/utils/dynamic_schema.py +483 -0
- digitalkin/utils/llm_ready_schema.py +75 -0
- digitalkin/utils/package_discover.py +357 -0
- digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
- digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
- digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
- digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
- digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
- modules/__init__.py +0 -0
- modules/cpu_intensive_module.py +280 -0
- modules/dynamic_setup_module.py +338 -0
- modules/minimal_llm_module.py +347 -0
- modules/text_transform_module.py +203 -0
- services/filesystem_module.py +200 -0
- 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
|