digitalkin 0.2.25rc0__py3-none-any.whl → 0.3.2.dev14__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 (122) hide show
  1. base_server/server_async_insecure.py +6 -5
  2. base_server/server_async_secure.py +6 -5
  3. base_server/server_sync_insecure.py +5 -4
  4. base_server/server_sync_secure.py +5 -4
  5. digitalkin/__version__.py +1 -1
  6. digitalkin/core/__init__.py +1 -0
  7. digitalkin/core/common/__init__.py +9 -0
  8. digitalkin/core/common/factories.py +156 -0
  9. digitalkin/core/job_manager/__init__.py +1 -0
  10. digitalkin/{modules → core}/job_manager/base_job_manager.py +138 -32
  11. digitalkin/core/job_manager/single_job_manager.py +373 -0
  12. digitalkin/{modules → core}/job_manager/taskiq_broker.py +121 -26
  13. digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
  14. digitalkin/core/task_manager/__init__.py +1 -0
  15. digitalkin/core/task_manager/base_task_manager.py +539 -0
  16. digitalkin/core/task_manager/local_task_manager.py +108 -0
  17. digitalkin/core/task_manager/remote_task_manager.py +87 -0
  18. digitalkin/core/task_manager/surrealdb_repository.py +266 -0
  19. digitalkin/core/task_manager/task_executor.py +249 -0
  20. digitalkin/core/task_manager/task_session.py +368 -0
  21. digitalkin/grpc_servers/__init__.py +1 -19
  22. digitalkin/grpc_servers/_base_server.py +3 -3
  23. digitalkin/grpc_servers/module_server.py +120 -195
  24. digitalkin/grpc_servers/module_servicer.py +81 -44
  25. digitalkin/grpc_servers/utils/__init__.py +1 -0
  26. digitalkin/grpc_servers/utils/exceptions.py +0 -8
  27. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +25 -9
  28. digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
  29. digitalkin/grpc_servers/utils/utility_schema_extender.py +100 -0
  30. digitalkin/logger.py +64 -27
  31. digitalkin/mixins/__init__.py +19 -0
  32. digitalkin/mixins/base_mixin.py +10 -0
  33. digitalkin/mixins/callback_mixin.py +24 -0
  34. digitalkin/mixins/chat_history_mixin.py +110 -0
  35. digitalkin/mixins/cost_mixin.py +76 -0
  36. digitalkin/mixins/file_history_mixin.py +93 -0
  37. digitalkin/mixins/filesystem_mixin.py +46 -0
  38. digitalkin/mixins/logger_mixin.py +51 -0
  39. digitalkin/mixins/storage_mixin.py +79 -0
  40. digitalkin/models/__init__.py +1 -1
  41. digitalkin/models/core/__init__.py +1 -0
  42. digitalkin/{modules/job_manager → models/core}/job_manager_models.py +3 -11
  43. digitalkin/models/core/task_monitor.py +74 -0
  44. digitalkin/models/grpc_servers/__init__.py +1 -0
  45. digitalkin/{grpc_servers/utils → models/grpc_servers}/models.py +92 -7
  46. digitalkin/models/module/__init__.py +18 -11
  47. digitalkin/models/module/base_types.py +61 -0
  48. digitalkin/models/module/module.py +9 -1
  49. digitalkin/models/module/module_context.py +282 -6
  50. digitalkin/models/module/module_types.py +29 -105
  51. digitalkin/models/module/setup_types.py +490 -0
  52. digitalkin/models/module/tool_cache.py +68 -0
  53. digitalkin/models/module/tool_reference.py +117 -0
  54. digitalkin/models/module/utility.py +167 -0
  55. digitalkin/models/services/__init__.py +9 -0
  56. digitalkin/models/services/cost.py +1 -0
  57. digitalkin/models/services/registry.py +35 -0
  58. digitalkin/models/services/storage.py +39 -5
  59. digitalkin/modules/__init__.py +5 -1
  60. digitalkin/modules/_base_module.py +265 -167
  61. digitalkin/modules/archetype_module.py +6 -1
  62. digitalkin/modules/tool_module.py +16 -3
  63. digitalkin/modules/trigger_handler.py +7 -6
  64. digitalkin/modules/triggers/__init__.py +8 -0
  65. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  66. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  67. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  68. digitalkin/services/__init__.py +4 -0
  69. digitalkin/services/communication/__init__.py +7 -0
  70. digitalkin/services/communication/communication_strategy.py +76 -0
  71. digitalkin/services/communication/default_communication.py +101 -0
  72. digitalkin/services/communication/grpc_communication.py +234 -0
  73. digitalkin/services/cost/__init__.py +9 -2
  74. digitalkin/services/cost/grpc_cost.py +9 -42
  75. digitalkin/services/filesystem/default_filesystem.py +0 -2
  76. digitalkin/services/filesystem/grpc_filesystem.py +10 -39
  77. digitalkin/services/registry/__init__.py +22 -1
  78. digitalkin/services/registry/default_registry.py +135 -4
  79. digitalkin/services/registry/exceptions.py +47 -0
  80. digitalkin/services/registry/grpc_registry.py +306 -0
  81. digitalkin/services/registry/registry_models.py +15 -0
  82. digitalkin/services/registry/registry_strategy.py +88 -4
  83. digitalkin/services/services_config.py +25 -3
  84. digitalkin/services/services_models.py +5 -1
  85. digitalkin/services/setup/default_setup.py +6 -7
  86. digitalkin/services/setup/grpc_setup.py +52 -15
  87. digitalkin/services/storage/grpc_storage.py +4 -4
  88. digitalkin/services/user_profile/__init__.py +12 -0
  89. digitalkin/services/user_profile/default_user_profile.py +55 -0
  90. digitalkin/services/user_profile/grpc_user_profile.py +69 -0
  91. digitalkin/services/user_profile/user_profile_strategy.py +25 -0
  92. digitalkin/utils/__init__.py +28 -0
  93. digitalkin/utils/arg_parser.py +1 -1
  94. digitalkin/utils/development_mode_action.py +2 -2
  95. digitalkin/utils/dynamic_schema.py +483 -0
  96. digitalkin/utils/package_discover.py +1 -2
  97. digitalkin/utils/schema_splitter.py +207 -0
  98. {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/METADATA +11 -30
  99. digitalkin-0.3.2.dev14.dist-info/RECORD +143 -0
  100. {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/top_level.txt +1 -0
  101. modules/archetype_with_tools_module.py +244 -0
  102. modules/cpu_intensive_module.py +1 -1
  103. modules/dynamic_setup_module.py +338 -0
  104. modules/minimal_llm_module.py +1 -1
  105. modules/text_transform_module.py +1 -1
  106. monitoring/digitalkin_observability/__init__.py +46 -0
  107. monitoring/digitalkin_observability/http_server.py +150 -0
  108. monitoring/digitalkin_observability/interceptors.py +176 -0
  109. monitoring/digitalkin_observability/metrics.py +201 -0
  110. monitoring/digitalkin_observability/prometheus.py +137 -0
  111. monitoring/tests/test_metrics.py +172 -0
  112. services/filesystem_module.py +7 -5
  113. services/storage_module.py +4 -2
  114. digitalkin/grpc_servers/registry_server.py +0 -65
  115. digitalkin/grpc_servers/registry_servicer.py +0 -456
  116. digitalkin/grpc_servers/utils/factory.py +0 -180
  117. digitalkin/modules/job_manager/single_job_manager.py +0 -294
  118. digitalkin/modules/job_manager/taskiq_job_manager.py +0 -290
  119. digitalkin-0.2.25rc0.dist-info/RECORD +0 -89
  120. /digitalkin/{grpc_servers/utils → models/grpc_servers}/types.py +0 -0
  121. {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/WHEEL +0 -0
  122. {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/licenses/LICENSE +0 -0
@@ -9,8 +9,9 @@ from pathlib import Path
9
9
  # Add parent directory to path to enable imports
10
10
  sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
11
11
 
12
- from digitalkin.grpc_servers._base_server import BaseServer
13
12
  from digitalkin.grpc_servers.utils.models import SecurityMode, ServerConfig, ServerMode
13
+
14
+ from digitalkin.grpc_servers._base_server import BaseServer
14
15
  from examples.base_server.mock.mock_pb2 import DESCRIPTOR, HelloReply # type: ignore
15
16
  from examples.base_server.mock.mock_pb2_grpc import (
16
17
  Greeter,
@@ -30,7 +31,7 @@ class AsyncGreeterImpl(Greeter):
30
31
 
31
32
  async def SayHello(self, request, context): # noqa: N802
32
33
  """Asynchronous implementation of SayHello method."""
33
- logger.info(f"Received request object: {request}")
34
+ logger.info("Received request object: %s", request)
34
35
  logger.info(f"Request attributes: {vars(request)}")
35
36
  logger.info(f"Received request with name: {request.name}")
36
37
 
@@ -40,7 +41,7 @@ class AsyncGreeterImpl(Greeter):
40
41
  name = "unknown"
41
42
  # Check context metadata
42
43
  for key, value in context.invocation_metadata():
43
- logger.info(f"Metadata: {key}={value}")
44
+ logger.info("Metadata: %s=%s", key, value)
44
45
  if key.lower() == "name":
45
46
  name = value
46
47
 
@@ -97,7 +98,7 @@ async def main_async() -> int:
97
98
  # as the KeyboardInterrupt usually breaks out of asyncio.run()
98
99
  logger.info("Server stopping due to keyboard interrupt...")
99
100
  except Exception as e:
100
- logger.exception(f"Error running server: {e}")
101
+ logger.exception("Error running server: %s", e)
101
102
  return 1
102
103
  finally:
103
104
  # Clean up resources if server was started
@@ -116,7 +117,7 @@ def main():
116
117
  logger.info("Server stopped by keyboard interrupt")
117
118
  return 0 # Clean exit
118
119
  except Exception as e:
119
- logger.exception(f"Fatal error: {e}")
120
+ logger.exception("Fatal error: %s", e)
120
121
  return 1
121
122
 
122
123
 
@@ -9,13 +9,14 @@ from pathlib import Path
9
9
  # Add parent directory to path to enable imports
10
10
  sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
11
11
 
12
- from digitalkin.grpc_servers._base_server import BaseServer
13
12
  from digitalkin.grpc_servers.utils.models import (
14
13
  SecurityMode,
15
14
  ServerConfig,
16
15
  ServerCredentials,
17
16
  ServerMode,
18
17
  )
18
+
19
+ from digitalkin.grpc_servers._base_server import BaseServer
19
20
  from examples.base_server.mock.mock_pb2 import DESCRIPTOR, HelloReply # type: ignore
20
21
  from examples.base_server.mock.mock_pb2_grpc import (
21
22
  Greeter,
@@ -35,7 +36,7 @@ class AsyncGreeterImpl(Greeter):
35
36
 
36
37
  async def SayHello(self, request, context): # noqa: N802
37
38
  """Asynchronous implementation of SayHello method."""
38
- logger.info(f"Received request object: {request}")
39
+ logger.info("Received request object: %s", request)
39
40
  logger.info(f"Request attributes: {vars(request)}")
40
41
  logger.info(f"Received request with name: {request.name}")
41
42
 
@@ -45,7 +46,7 @@ class AsyncGreeterImpl(Greeter):
45
46
  name = "unknown"
46
47
  # Check context metadata
47
48
  for key, value in context.invocation_metadata():
48
- logger.info(f"Metadata: {key}={value}")
49
+ logger.info("Metadata: %s=%s", key, value)
49
50
  if key.lower() == "name":
50
51
  name = value
51
52
 
@@ -115,7 +116,7 @@ async def main_async() -> int:
115
116
  # as the KeyboardInterrupt usually breaks out of asyncio.run()
116
117
  logger.info("Server stopping due to keyboard interrupt...")
117
118
  except Exception as e:
118
- logger.exception(f"Error running server: {e}")
119
+ logger.exception("Error running server: %s", e)
119
120
  return 1
120
121
  finally:
121
122
  # Clean up resources if server was started
@@ -134,7 +135,7 @@ def main():
134
135
  logger.info("Server stopped by keyboard interrupt")
135
136
  return 0 # Clean exit
136
137
  except Exception as e:
137
- logger.exception(f"Fatal error: {e}")
138
+ logger.exception("Fatal error: %s", e)
138
139
  return 1
139
140
 
140
141
 
@@ -8,8 +8,9 @@ from pathlib import Path
8
8
  # Add parent directory to path to enable imports
9
9
  sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
10
10
 
11
- from digitalkin.grpc_servers._base_server import BaseServer
12
11
  from digitalkin.grpc_servers.utils.models import SecurityMode, ServerConfig, ServerMode
12
+
13
+ from digitalkin.grpc_servers._base_server import BaseServer
13
14
  from examples.base_server.mock.mock_pb2 import DESCRIPTOR, HelloReply # type: ignore
14
15
  from examples.base_server.mock.mock_pb2_grpc import (
15
16
  Greeter,
@@ -29,7 +30,7 @@ class SyncGreeterServicer(Greeter):
29
30
 
30
31
  def SayHello(self, request, context): # noqa: N802
31
32
  """Implementation of SayHello method."""
32
- logger.info(f"Received request object: {request}")
33
+ logger.info("Received request object: %s", request)
33
34
  logger.info(f"Request attributes: {vars(request)}")
34
35
  logger.info(f"Received request with name: {request.name}")
35
36
 
@@ -39,7 +40,7 @@ class SyncGreeterServicer(Greeter):
39
40
  name = "unknown"
40
41
  # Check context metadata
41
42
  for key, value in context.invocation_metadata():
42
- logger.info(f"Metadata: {key}={value}")
43
+ logger.info("Metadata: %s=%s", key, value)
43
44
  if key.lower() == "name":
44
45
  name = value
45
46
 
@@ -92,7 +93,7 @@ def main() -> int:
92
93
  server.stop()
93
94
 
94
95
  except Exception as e:
95
- logger.exception(f"Error running server: {e}")
96
+ logger.exception("Error running server: %s", e)
96
97
  return 1
97
98
 
98
99
  return 0
@@ -8,13 +8,14 @@ from pathlib import Path
8
8
  # Add parent directory to path to enable imports
9
9
  sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
10
10
 
11
- from digitalkin.grpc_servers._base_server import BaseServer
12
11
  from digitalkin.grpc_servers.utils.models import (
13
12
  SecurityMode,
14
13
  ServerConfig,
15
14
  ServerCredentials,
16
15
  ServerMode,
17
16
  )
17
+
18
+ from digitalkin.grpc_servers._base_server import BaseServer
18
19
  from examples.base_server.mock.mock_pb2 import DESCRIPTOR, HelloReply # type: ignore
19
20
  from examples.base_server.mock.mock_pb2_grpc import (
20
21
  Greeter,
@@ -34,7 +35,7 @@ class SyncGreeterServicer(Greeter):
34
35
 
35
36
  def SayHello(self, request, context): # noqa: N802
36
37
  """Implementation of SayHello method."""
37
- logger.info(f"Received request object: {request}")
38
+ logger.info("Received request object: %s", request)
38
39
  logger.info(f"Request attributes: {vars(request)}")
39
40
  logger.info(f"Received request with name: {request.name}")
40
41
 
@@ -44,7 +45,7 @@ class SyncGreeterServicer(Greeter):
44
45
  name = "unknown"
45
46
  # Check context metadata
46
47
  for key, value in context.invocation_metadata():
47
- logger.info(f"Metadata: {key}={value}")
48
+ logger.info("Metadata: %s=%s", key, value)
48
49
  if key.lower() == "name":
49
50
  name = value
50
51
 
@@ -111,7 +112,7 @@ def main() -> int:
111
112
  server.stop()
112
113
 
113
114
  except Exception as e:
114
- logger.exception(f"Error running server: {e}")
115
+ logger.exception("Error running server: %s", e)
115
116
  return 1
116
117
 
117
118
  return 0
digitalkin/__version__.py CHANGED
@@ -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.25rc0"
8
+ __version__ = "0.3.2.dev14"
@@ -0,0 +1 @@
1
+ """Core of Digitlakin defining the task management and sub-modules."""
@@ -0,0 +1,9 @@
1
+ """Common utilities for the core module."""
2
+
3
+ from digitalkin.core.common.factories import ConnectionFactory, ModuleFactory, QueueFactory
4
+
5
+ __all__ = [
6
+ "ConnectionFactory",
7
+ "ModuleFactory",
8
+ "QueueFactory",
9
+ ]
@@ -0,0 +1,156 @@
1
+ """Common factory functions for reducing code duplication in core module."""
2
+
3
+ import asyncio
4
+ import datetime
5
+
6
+ from digitalkin.core.task_manager.surrealdb_repository import SurrealDBConnection
7
+ from digitalkin.logger import logger
8
+ from digitalkin.modules._base_module import BaseModule
9
+
10
+
11
+ class ConnectionFactory:
12
+ """Factory for creating SurrealDB connections with consistent configuration."""
13
+
14
+ @staticmethod
15
+ async def create_surreal_connection(
16
+ database: str = "task_manager",
17
+ timeout: datetime.timedelta = datetime.timedelta(seconds=5),
18
+ *,
19
+ auto_init: bool = True,
20
+ ) -> SurrealDBConnection:
21
+ """Create and optionally initialize a SurrealDB connection.
22
+
23
+ This factory method centralizes the creation of SurrealDB connections
24
+ to ensure consistent configuration across the codebase.
25
+
26
+ Args:
27
+ database: Database name to connect to
28
+ timeout: Connection timeout
29
+ auto_init: Whether to automatically initialize the connection
30
+
31
+ Returns:
32
+ Initialized or uninitialized SurrealDBConnection instance
33
+
34
+ Example:
35
+ # Create and auto-initialize
36
+ conn = await ConnectionFactory.create_surreal_connection("taskiq_worker")
37
+
38
+ # Create without initialization
39
+ conn = await ConnectionFactory.create_surreal_connection(auto_init=False)
40
+ await conn.init_surreal_instance()
41
+ """
42
+ logger.debug(
43
+ "Creating SurrealDB connection for database: %s, timeout: %s",
44
+ database,
45
+ timeout,
46
+ extra={"database": database, "timeout": str(timeout)},
47
+ )
48
+
49
+ connection: SurrealDBConnection = SurrealDBConnection(database, timeout)
50
+
51
+ if auto_init:
52
+ await connection.init_surreal_instance()
53
+ logger.debug("SurrealDB connection initialized for database: %s", database)
54
+
55
+ return connection
56
+
57
+
58
+ class ModuleFactory:
59
+ """Factory for creating module instances with consistent configuration."""
60
+
61
+ @staticmethod
62
+ def create_module_instance(
63
+ module_class: type[BaseModule],
64
+ job_id: str,
65
+ mission_id: str,
66
+ setup_id: str,
67
+ setup_version_id: str,
68
+ ) -> BaseModule:
69
+ """Create a module instance with standard parameters.
70
+
71
+ This factory method centralizes module instantiation to ensure
72
+ consistent parameter passing across the codebase.
73
+
74
+ Args:
75
+ module_class: The module class to instantiate
76
+ job_id: Unique job identifier
77
+ mission_id: Mission identifier
78
+ setup_id: Setup identifier
79
+ setup_version_id: Setup version identifier
80
+
81
+ Returns:
82
+ Instantiated module
83
+
84
+ Raises:
85
+ ValueError: If job_id or mission_id is empty
86
+
87
+ Example:
88
+ module = ModuleFactory.create_module_instance(
89
+ MyModule,
90
+ job_id="job_123",
91
+ mission_id="mission:test",
92
+ setup_id="setup:config",
93
+ setup_version_id="v1.0",
94
+ )
95
+ """
96
+ # Validate parameters
97
+ if not job_id:
98
+ msg = "job_id cannot be empty"
99
+ raise ValueError(msg)
100
+ if not mission_id:
101
+ msg = "mission_id cannot be empty"
102
+ raise ValueError(msg)
103
+
104
+ logger.debug(
105
+ "Creating module instance: %s for job: %s",
106
+ module_class.__name__,
107
+ job_id,
108
+ extra={
109
+ "module_class": module_class.__name__,
110
+ "job_id": job_id,
111
+ "mission_id": mission_id,
112
+ "setup_id": setup_id,
113
+ "setup_version_id": setup_version_id,
114
+ },
115
+ )
116
+
117
+ return module_class(
118
+ job_id=job_id,
119
+ mission_id=mission_id,
120
+ setup_id=setup_id,
121
+ setup_version_id=setup_version_id,
122
+ )
123
+
124
+
125
+ class QueueFactory:
126
+ """Factory for creating asyncio queues with consistent configuration."""
127
+
128
+ # Default max queue size to prevent unbounded memory growth
129
+ DEFAULT_MAX_QUEUE_SIZE = 1000
130
+
131
+ @staticmethod
132
+ def create_bounded_queue(maxsize: int = DEFAULT_MAX_QUEUE_SIZE) -> asyncio.Queue:
133
+ """Create a bounded asyncio queue with standard configuration.
134
+
135
+ Args:
136
+ maxsize: Maximum queue size (default 1000, 0 means unlimited)
137
+
138
+ Returns:
139
+ Bounded asyncio.Queue instance
140
+
141
+ Raises:
142
+ ValueError: If maxsize is negative
143
+
144
+ Example:
145
+ queue = QueueFactory.create_bounded_queue()
146
+ # or with custom size
147
+ queue = QueueFactory.create_bounded_queue(maxsize=500)
148
+ # unlimited queue
149
+ queue = QueueFactory.create_bounded_queue(maxsize=0)
150
+ """
151
+ if maxsize < 0:
152
+ msg = "maxsize must be >= 0"
153
+ raise ValueError(msg)
154
+
155
+ logger.debug("Creating bounded queue with maxsize: %d", maxsize, extra={"maxsize": maxsize})
156
+ return asyncio.Queue(maxsize=maxsize)
@@ -0,0 +1 @@
1
+ """Job Manager logic."""
@@ -5,17 +5,126 @@ from collections.abc import AsyncGenerator, AsyncIterator, Callable, Coroutine
5
5
  from contextlib import asynccontextmanager
6
6
  from typing import Any, Generic
7
7
 
8
- from digitalkin.models import ModuleStatus
9
- from digitalkin.models.module import InputModelT, OutputModelT, SetupModelT
8
+ from digitalkin.core.task_manager.base_task_manager import BaseTaskManager
9
+ from digitalkin.core.task_manager.task_session import TaskSession
10
+ from digitalkin.models.core.task_monitor import TaskStatus
11
+ from digitalkin.models.module.module import ModuleCodeModel
12
+ from digitalkin.models.module.module_types import InputModelT, OutputModelT, SetupModelT
10
13
  from digitalkin.modules._base_module import BaseModule
11
14
  from digitalkin.services.services_config import ServicesConfig
12
15
  from digitalkin.services.services_models import ServicesMode
13
16
 
14
17
 
15
- class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
16
- """Abstract base class for managing background module jobs."""
18
+ class BaseJobManager(abc.ABC, Generic[InputModelT, OutputModelT, SetupModelT]):
19
+ """Abstract base class for managing background module jobs.
17
20
 
18
- async def _start(self) -> None:
21
+ Uses composition to delegate task lifecycle management to a TaskManager.
22
+ """
23
+
24
+ module_class: type[BaseModule]
25
+ services_mode: ServicesMode
26
+ _task_manager: BaseTaskManager
27
+
28
+ def __init__(
29
+ self,
30
+ module_class: type[BaseModule],
31
+ services_mode: ServicesMode,
32
+ task_manager: BaseTaskManager,
33
+ ) -> None:
34
+ """Initialize the job manager.
35
+
36
+ Args:
37
+ module_class: The class of the module to be managed.
38
+ services_mode: The mode of operation for the services (e.g., ASYNC or SYNC).
39
+ task_manager: The task manager instance to use for task lifecycle management.
40
+ """
41
+ self.module_class = module_class
42
+ self.services_mode = services_mode
43
+ self._task_manager = task_manager
44
+
45
+ services_config = ServicesConfig(
46
+ services_config_strategies=self.module_class.services_config_strategies,
47
+ services_config_params=self.module_class.services_config_params,
48
+ mode=services_mode,
49
+ )
50
+ setattr(self.module_class, "services_config", services_config)
51
+
52
+ # Properties to expose task manager attributes
53
+ @property
54
+ def tasks_sessions(self) -> dict[str, TaskSession]:
55
+ """Get task sessions from the task manager."""
56
+ return self._task_manager.tasks_sessions
57
+
58
+ @property
59
+ def tasks(self) -> dict[str, Any]:
60
+ """Get tasks from the task manager."""
61
+ return self._task_manager.tasks
62
+
63
+ # Delegate task lifecycle methods to task manager
64
+ async def create_task(
65
+ self,
66
+ task_id: str,
67
+ mission_id: str,
68
+ module: BaseModule,
69
+ coro: Coroutine[Any, Any, None],
70
+ **kwargs: Any, # noqa: ANN401
71
+ ) -> None:
72
+ """Create a task using the task manager.
73
+
74
+ Args:
75
+ task_id: Unique identifier for the task
76
+ mission_id: Mission identifier
77
+ module: Module instance
78
+ coro: Coroutine to execute
79
+ **kwargs: Additional arguments for task creation
80
+ """
81
+ await self._task_manager.create_task(task_id, mission_id, module, coro, **kwargs)
82
+
83
+ async def clean_session(self, task_id: str, mission_id: str) -> bool:
84
+ """Clean a task's session.
85
+
86
+ Args:
87
+ task_id: Unique identifier for the task.
88
+ mission_id: Mission identifier.
89
+
90
+ Returns:
91
+ bool: True if the task was successfully cancelled, False otherwise.
92
+ """
93
+ return await self._task_manager.clean_session(task_id, mission_id)
94
+
95
+ async def cancel_task(self, task_id: str, mission_id: str, timeout: float | None = None) -> bool:
96
+ """Cancel a task.
97
+
98
+ Args:
99
+ task_id: Unique identifier for the task.
100
+ mission_id: Mission identifier.
101
+ timeout: Optional timeout in seconds to wait for the cancellation to complete.
102
+
103
+ Returns:
104
+ bool: True if the task was successfully cancelled, False otherwise.
105
+ """
106
+ return await self._task_manager.cancel_task(task_id, mission_id, timeout)
107
+
108
+ async def send_signal(self, task_id: str, mission_id: str, signal_type: str, payload: dict) -> bool:
109
+ """Send signal to a task.
110
+
111
+ Args:
112
+ task_id: Unique identifier for the task.
113
+ mission_id: Mission identifier.
114
+ signal_type: Type of signal to send.
115
+ payload: Payload data for the signal.
116
+
117
+ Returns:
118
+ bool: True if the signal was successfully sent, False otherwise.
119
+ """
120
+ return await self._task_manager.send_signal(task_id, mission_id, signal_type, payload)
121
+
122
+ async def shutdown(self, mission_id: str, timeout: float = 30.0) -> None:
123
+ """Shutdown all tasks."""
124
+ await self._task_manager.shutdown(mission_id, timeout)
125
+
126
+ @abc.abstractmethod
127
+ async def start(self) -> None:
19
128
  """Start the job manager.
20
129
 
21
130
  This method initializes any necessary resources or configurations
@@ -24,8 +133,9 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
24
133
 
25
134
  @staticmethod
26
135
  async def job_specific_callback(
27
- callback: Callable[[str, OutputModelT], Coroutine[Any, Any, None]], job_id: str
28
- ) -> Callable[[OutputModelT], Coroutine[Any, Any, None]]:
136
+ callback: Callable[[str, OutputModelT | ModuleCodeModel], Coroutine[Any, Any, None]],
137
+ job_id: str,
138
+ ) -> Callable[[OutputModelT | ModuleCodeModel], Coroutine[Any, Any, None]]:
29
139
  """Generate a job-specific callback function.
30
140
 
31
141
  Args:
@@ -36,7 +146,7 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
36
146
  Callable: A wrapped callback function that includes the job ID.
37
147
  """
38
148
 
39
- def callback_wrapper(output_data: OutputModelT) -> Coroutine[Any, Any, None]:
149
+ def callback_wrapper(output_data: OutputModelT | ModuleCodeModel) -> Coroutine[Any, Any, None]:
40
150
  """Wrapper for the callback function.
41
151
 
42
152
  Args:
@@ -49,26 +159,6 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
49
159
 
50
160
  return callback_wrapper
51
161
 
52
- def __init__(
53
- self,
54
- module_class: type[BaseModule],
55
- services_mode: ServicesMode,
56
- ) -> None:
57
- """Initialize the job manager.
58
-
59
- Args:
60
- module_class: The class of the module to be managed.
61
- services_mode: The mode of operation for the services (e.g., ASYNC or SYNC).
62
- """
63
- self.module_class = module_class
64
-
65
- services_config = ServicesConfig(
66
- services_config_strategies=self.module_class.services_config_strategies,
67
- services_config_params=self.module_class.services_config_params,
68
- mode=services_mode,
69
- )
70
- setattr(self.module_class, "services_config", services_config)
71
-
72
162
  @abc.abstractmethod # type: ignore
73
163
  @asynccontextmanager # type: ignore
74
164
  async def generate_stream_consumer(self, job_id: str) -> AsyncIterator[AsyncGenerator[dict[str, Any], None]]:
@@ -104,7 +194,7 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
104
194
  """
105
195
 
106
196
  @abc.abstractmethod
107
- async def generate_config_setup_module_response(self, job_id: str) -> SetupModelT:
197
+ async def generate_config_setup_module_response(self, job_id: str) -> SetupModelT | ModuleCodeModel:
108
198
  """Generate a stream consumer for a module's output data.
109
199
 
110
200
  This method creates an asynchronous generator that streams output data
@@ -115,7 +205,7 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
115
205
  job_id: The unique identifier of the job.
116
206
 
117
207
  Returns:
118
- SetupModelT: the SetupModelT object fully processed.
208
+ SetupModelT | ModuleCodeModel: the SetupModelT object fully processed, or an error code.
119
209
  """
120
210
 
121
211
  @abc.abstractmethod
@@ -156,14 +246,30 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
156
246
  """
157
247
 
158
248
  @abc.abstractmethod
159
- async def get_module_status(self, job_id: str) -> ModuleStatus | None:
249
+ async def get_module_status(self, job_id: str) -> TaskStatus:
160
250
  """Retrieve the status of a module job.
161
251
 
162
252
  Args:
163
253
  job_id: The unique identifier of the job.
164
254
 
165
255
  Returns:
166
- ModuleStatus | None: The status of the job, or None if the job does not exist.
256
+ ModuleStatu: The status of the job.
257
+ """
258
+
259
+ @abc.abstractmethod
260
+ async def wait_for_completion(self, job_id: str) -> None:
261
+ """Wait for a task to complete.
262
+
263
+ This method blocks until the specified job has reached a terminal state.
264
+ The implementation varies by job manager type:
265
+ - SingleJobManager: Awaits the asyncio.Task directly
266
+ - TaskiqJobManager: Polls task status from SurrealDB
267
+
268
+ Args:
269
+ job_id: The unique identifier of the job to wait for.
270
+
271
+ Raises:
272
+ KeyError: If the job_id is not found.
167
273
  """
168
274
 
169
275
  @abc.abstractmethod