digitalkin 0.2.12__py3-none-any.whl → 0.2.13__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 (36) hide show
  1. digitalkin/__version__.py +1 -1
  2. digitalkin/grpc_servers/_base_server.py +15 -17
  3. digitalkin/grpc_servers/module_server.py +9 -10
  4. digitalkin/grpc_servers/module_servicer.py +107 -85
  5. digitalkin/grpc_servers/registry_server.py +3 -6
  6. digitalkin/grpc_servers/registry_servicer.py +18 -19
  7. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +3 -5
  8. digitalkin/logger.py +45 -1
  9. digitalkin/models/module/module.py +1 -0
  10. digitalkin/modules/_base_module.py +44 -5
  11. digitalkin/modules/job_manager/base_job_manager.py +139 -0
  12. digitalkin/modules/job_manager/job_manager_models.py +44 -0
  13. digitalkin/modules/job_manager/single_job_manager.py +218 -0
  14. digitalkin/modules/job_manager/taskiq_broker.py +173 -0
  15. digitalkin/modules/job_manager/taskiq_job_manager.py +213 -0
  16. digitalkin/services/cost/default_cost.py +8 -4
  17. digitalkin/services/cost/grpc_cost.py +15 -7
  18. digitalkin/services/filesystem/default_filesystem.py +2 -4
  19. digitalkin/services/filesystem/grpc_filesystem.py +8 -5
  20. digitalkin/services/setup/__init__.py +1 -0
  21. digitalkin/services/setup/default_setup.py +10 -12
  22. digitalkin/services/setup/grpc_setup.py +8 -10
  23. digitalkin/services/storage/default_storage.py +11 -5
  24. digitalkin/services/storage/grpc_storage.py +23 -8
  25. digitalkin/utils/arg_parser.py +5 -48
  26. digitalkin/utils/development_mode_action.py +51 -0
  27. {digitalkin-0.2.12.dist-info → digitalkin-0.2.13.dist-info}/METADATA +42 -11
  28. {digitalkin-0.2.12.dist-info → digitalkin-0.2.13.dist-info}/RECORD +35 -28
  29. {digitalkin-0.2.12.dist-info → digitalkin-0.2.13.dist-info}/WHEEL +1 -1
  30. modules/cpu_intensive_module.py +271 -0
  31. modules/minimal_llm_module.py +200 -56
  32. modules/storage_module.py +5 -6
  33. modules/text_transform_module.py +1 -1
  34. digitalkin/modules/job_manager.py +0 -177
  35. {digitalkin-0.2.12.dist-info → digitalkin-0.2.13.dist-info}/licenses/LICENSE +0 -0
  36. {digitalkin-0.2.12.dist-info → digitalkin-0.2.13.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,6 @@ which handles registration, deregistration, discovery, and status management
5
5
  of DigitalKin modules.
6
6
  """
7
7
 
8
- import logging
9
8
  from collections.abc import Iterator
10
9
  from enum import Enum
11
10
 
@@ -20,7 +19,7 @@ from digitalkin_proto.digitalkin.module_registry.v2 import (
20
19
  from pydantic import BaseModel
21
20
  from typing_extensions import Self
22
21
 
23
- logger = logging.getLogger(__name__)
22
+ from digitalkin.logger import logger
24
23
 
25
24
 
26
25
  class ExtendedEnum(Enum):
@@ -184,7 +183,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
184
183
  registration_pb2.RegisterResponse: A response indicating success or failure.
185
184
  """
186
185
  module_id = request.module_id
187
- logger.info("Registering module: %s", module_id)
186
+ logger.debug("Registering module: %s", module_id)
188
187
 
189
188
  # Check if module is already registered
190
189
  if module_id in self.registered_modules:
@@ -207,7 +206,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
207
206
  message=None,
208
207
  )
209
208
 
210
- logger.info("Module %s registered at %s:%d", module_id, request.address, request.port)
209
+ logger.debug("Module %s registered at %s:%d", module_id, request.address, request.port)
211
210
  return registration_pb2.RegisterResponse(success=True)
212
211
 
213
212
  def DeregisterModule( # noqa: N802
@@ -228,7 +227,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
228
227
  registration_pb2.DeregisterResponse: A response indicating success or failure.
229
228
  """
230
229
  module_id = request.module_id
231
- logger.info("Deregistering module: %s", module_id)
230
+ logger.debug("Deregistering module: %s", module_id)
232
231
 
233
232
  # Check if module exists in registry
234
233
  if module_id not in self.registered_modules:
@@ -242,7 +241,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
242
241
  # Remove the module
243
242
  del self.registered_modules[module_id]
244
243
 
245
- logger.info("Module %s deregistered", module_id)
244
+ logger.debug("Module %s deregistered", module_id)
246
245
  return registration_pb2.DeregisterResponse(success=True)
247
246
 
248
247
  def DiscoverInfoModule( # noqa: N802
@@ -261,7 +260,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
261
260
  Returns:
262
261
  discover_pb2.DiscoverInfoResponse: A response containing the module's information.
263
262
  """
264
- logger.info("Discovering module: %s", request.module_id)
263
+ logger.debug("Discovering module: %s", request.module_id)
265
264
 
266
265
  # Check if module exists in registry
267
266
  if request.module_id not in self.registered_modules:
@@ -289,24 +288,24 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
289
288
  Returns:
290
289
  discover_pb2.DiscoverSearchResponse: A response containing matching modules.
291
290
  """
292
- logger.info("Discovering modules with criteria:")
291
+ logger.debug("Discovering modules with criteria:")
293
292
 
294
293
  # Start with all modules
295
294
  results = list(self.registered_modules.values())
296
- logger.info("%s", list(results))
295
+ logger.debug("%s", list(results))
297
296
  # Filter by name if specified
298
297
  if request.name:
299
- logger.info("\tname %s", request.name)
298
+ logger.debug("\tname %s", request.name)
300
299
  results = [m for m in results if request.name in m.metadata.name]
301
300
 
302
301
  # Filter by type if specified
303
302
  if request.module_type:
304
- logger.info("\tmodule_type %s", request.module_type)
303
+ logger.debug("\tmodule_type %s", request.module_type)
305
304
  results = [m for m in results if m.module_type == request.module_type]
306
305
 
307
306
  # Filter by tags if specified
308
307
  if request.tags:
309
- logger.info("\ttags %s", request.tags)
308
+ logger.debug("\ttags %s", request.tags)
310
309
  results = [m for m in results if any(tag in m.metadata.tags for tag in request.tags)]
311
310
 
312
311
  # Filter by description if specified
@@ -315,7 +314,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
315
314
  results = [m for m in results if request.description in m.metadata.description]
316
315
  """
317
316
 
318
- logger.info("Found %d matching modules", len(results))
317
+ logger.debug("Found %d matching modules", len(results))
319
318
  return discover_pb2.DiscoverSearchResponse(modules=[r.to_proto() for r in results])
320
319
 
321
320
  def GetModuleStatus( # noqa: N802
@@ -334,7 +333,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
334
333
  Returns:
335
334
  status_pb2.ModuleStatusResponse: A response containing the module's status.
336
335
  """
337
- logger.info("Getting status for module: %s", request.module_id)
336
+ logger.debug("Getting status for module: %s", request.module_id)
338
337
 
339
338
  # Check if module exists in registry
340
339
  if request.module_id not in self.registered_modules:
@@ -363,7 +362,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
363
362
  Returns:
364
363
  status_pb2.ListModulesStatusResponse: A response containing a list of module statuses.
365
364
  """
366
- logger.info(
365
+ logger.debug(
367
366
  "Getting registered modules with offset %d and limit %d",
368
367
  request.offset,
369
368
  request.list_size,
@@ -384,7 +383,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
384
383
  for module in list(self.registered_modules.values())[request.offset : request.offset + list_size]
385
384
  ]
386
385
 
387
- logger.info("Found %d registered modules", len(modules_statuses))
386
+ logger.debug("Found %d registered modules", len(modules_statuses))
388
387
  return status_pb2.ListModulesStatusResponse(
389
388
  list_size=len(modules_statuses),
390
389
  modules_statuses=modules_statuses,
@@ -406,7 +405,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
406
405
  Yields:
407
406
  status_pb2.ModuleStatusResponse: Responses containing individual module statuses.
408
407
  """
409
- logger.info("Streaming all %d registered modules", len(self.registered_modules))
408
+ logger.debug("Streaming all %d registered modules", len(self.registered_modules))
410
409
  for module in self.registered_modules.values():
411
410
  yield status_pb2.ModuleStatusResponse(
412
411
  module_id=module.module_id,
@@ -431,7 +430,7 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
431
430
  status_pb2.UpdateStatusResponse: A response indicating success or failure.
432
431
  """
433
432
  module_id = request.module_id
434
- logger.info("Updating status for module: %s to %s", module_id, request.status)
433
+ logger.debug("Updating status for module: %s to %s", module_id, request.status)
435
434
 
436
435
  # Check if module exists in registry
437
436
  if request.module_id not in self.registered_modules:
@@ -453,5 +452,5 @@ class RegistryServicer(module_registry_service_pb2_grpc.ModuleRegistryServiceSer
453
452
  module_info = self.registered_modules[module_id]
454
453
  module_info.status = ModuleStatus(request.status)
455
454
 
456
- logger.info("Status for module %s updated to %s", module_id, request.status)
455
+ logger.debug("Status for module %s updated to %s", module_id, request.status)
457
456
  return status_pb2.UpdateStatusResponse(success=True)
@@ -1,6 +1,5 @@
1
1
  """Client wrapper to ease channel creation with specific ServerConfig."""
2
2
 
3
- import logging
4
3
  from pathlib import Path
5
4
  from typing import Any
6
5
 
@@ -8,8 +7,7 @@ import grpc
8
7
 
9
8
  from digitalkin.grpc_servers.utils.exceptions import ServerError
10
9
  from digitalkin.grpc_servers.utils.models import ClientConfig, SecurityMode
11
-
12
- logger = logging.getLogger(__name__)
10
+ from digitalkin.logger import logger
13
11
 
14
12
 
15
13
  class GrpcClientWrapper:
@@ -64,9 +62,9 @@ class GrpcClientWrapper:
64
62
  """
65
63
  try:
66
64
  # Call the register method
67
- logger.warning("send request to %s", query_endpoint)
65
+ logger.debug("send request to %s", query_endpoint)
68
66
  response = getattr(self.stub, query_endpoint)(request)
69
- logger.warning("recive response from request to registry: %s", response)
67
+ logger.debug("receive response from request to registry: %s", response)
70
68
  except grpc.RpcError:
71
69
  logger.exception("RPC error during registration:")
72
70
  raise ServerError
digitalkin/logger.py CHANGED
@@ -2,11 +2,46 @@
2
2
 
3
3
  import logging
4
4
  import sys
5
+ from typing import ClassVar
6
+
7
+
8
+ class ColorFormatter(logging.Formatter):
9
+ """Color formatter for logging."""
10
+
11
+ grey = "\x1b[38;20m"
12
+ green = "\x1b[32;20m"
13
+ blue = "\x1b[34;20m"
14
+ yellow = "\x1b[33;20m"
15
+ red = "\x1b[31;20m"
16
+ bold_red = "\x1b[31;1m"
17
+ reset = "\x1b[0m"
18
+ format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" # type: ignore
19
+
20
+ FORMATS: ClassVar[dict[int, str]] = {
21
+ logging.DEBUG: grey + format + reset + "\n", # type: ignore
22
+ logging.INFO: green + format + reset + "\n", # type: ignore
23
+ logging.WARNING: yellow + format + reset + "\n", # type: ignore
24
+ logging.ERROR: red + format + reset + "\n", # type: ignore
25
+ logging.CRITICAL: bold_red + format + reset + "\n", # type: ignore
26
+ }
27
+
28
+ def format(self, record: logging.LogRecord) -> str: # type: ignore
29
+ """Format the log record.
30
+
31
+ Args:
32
+ record: The log record to format.
33
+
34
+ Returns:
35
+ str: The formatted log record.
36
+ """
37
+ log_fmt = self.FORMATS.get(record.levelno)
38
+ formatter = logging.Formatter(log_fmt)
39
+ return formatter.format(record)
40
+
5
41
 
6
42
  logging.basicConfig(
7
43
  level=logging.DEBUG,
8
44
  stream=sys.stdout,
9
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
10
45
  datefmt="%Y-%m-%d %H:%M:%S",
11
46
  )
12
47
 
@@ -15,3 +50,12 @@ logging.getLogger("asyncio").setLevel(logging.DEBUG)
15
50
 
16
51
 
17
52
  logger = logging.getLogger("digitalkin")
53
+
54
+ if not logger.handlers:
55
+ ch = logging.StreamHandler()
56
+ ch.setLevel(logging.INFO)
57
+
58
+ ch.setFormatter(ColorFormatter())
59
+
60
+ logger.addHandler(ch)
61
+ logger.propagate = False
@@ -14,6 +14,7 @@ class ModuleStatus(Enum):
14
14
  STOPPING = auto() # Module is stopping
15
15
  STOPPED = auto() # Module stop successfuly
16
16
  FAILED = auto() # Module stopped due to internal error
17
+ CANCELLED = auto() # Module stopped due to internal error
17
18
  NOT_FOUND = auto()
18
19
 
19
20
 
@@ -7,6 +7,8 @@ from abc import ABC, abstractmethod
7
7
  from collections.abc import Callable, Coroutine
8
8
  from typing import Any, ClassVar, Generic
9
9
 
10
+ from pydantic import BaseModel
11
+
10
12
  from digitalkin.logger import logger
11
13
  from digitalkin.models.module import InputModelT, ModuleStatus, OutputModelT, SecretModelT, SetupModelT
12
14
  from digitalkin.services.agent.agent_strategy import AgentStrategy
@@ -20,6 +22,14 @@ from digitalkin.services.storage.storage_strategy import StorageStrategy
20
22
  from digitalkin.utils.llm_ready_schema import llm_ready_schema
21
23
 
22
24
 
25
+ class ModuleErrorModel(BaseModel):
26
+ """Typed error/code model."""
27
+
28
+ code: str
29
+ exception: str
30
+ short_description: str
31
+
32
+
23
33
  class BaseModule(ABC, Generic[InputModelT, OutputModelT, SetupModelT, SecretModelT]):
24
34
  """BaseModule is the abstract base for all modules in the DigitalKin SDK."""
25
35
 
@@ -223,30 +233,59 @@ class BaseModule(ABC, Generic[InputModelT, OutputModelT, SetupModelT, SecretMode
223
233
  asyncio.CancelledError: If the module is cancelled
224
234
  """
225
235
  try:
236
+ logger.warning("Starting module %s", self.name)
226
237
  await self.run(input_data, setup_data, callback)
227
- await self.stop()
238
+ logger.warning("Module %s finished", self.name)
228
239
  except asyncio.CancelledError:
229
- logger.info(f"Module {self.name} cancelled")
240
+ self._status = ModuleStatus.CANCELLED
241
+ logger.error(f"Module {self.name} cancelled")
230
242
  except Exception:
231
243
  self._status = ModuleStatus.FAILED
232
244
  logger.exception("Error inside module %s", self.name)
233
245
  else:
234
246
  self._status = ModuleStatus.STOPPED
247
+ finally:
248
+ await self.stop()
235
249
 
236
250
  async def start(
237
251
  self,
238
252
  input_data: InputModelT,
239
253
  setup_data: SetupModelT,
240
- callback: Callable[[OutputModelT], Coroutine[Any, Any, None]],
254
+ callback: Callable[[OutputModelT | ModuleErrorModel], Coroutine[Any, Any, None]],
255
+ done_callback: Callable | None = None,
241
256
  ) -> None:
242
257
  """Start the module."""
243
258
  try:
259
+ logger.info("Inititalize module")
244
260
  await self.initialize(setup_data=setup_data)
261
+ except Exception as e:
262
+ self._status = ModuleStatus.FAILED
263
+ short_description = "Error initializing module"
264
+ logger.exception("%s: %s", short_description, e)
265
+ await callback(
266
+ ModuleErrorModel(
267
+ code=str(self._status),
268
+ short_description=short_description,
269
+ exception=str(e),
270
+ )
271
+ )
272
+ if done_callback is not None:
273
+ await done_callback(None)
274
+ await self.stop()
275
+ return
276
+
277
+ try:
278
+ logger.info("Run lifecycle")
245
279
  self._status = ModuleStatus.RUNNING
246
- self._task = asyncio.create_task(self._run_lifecycle(input_data, setup_data, callback))
280
+ self._task = asyncio.create_task(
281
+ self._run_lifecycle(input_data, setup_data, callback),
282
+ name="module_lifecycle",
283
+ )
284
+ if done_callback is not None:
285
+ self._task.add_done_callback(done_callback)
247
286
  except Exception:
248
287
  self._status = ModuleStatus.FAILED
249
- logger.exception("Error starting module")
288
+ logger.exception("Error during module lifecyle")
250
289
 
251
290
  async def stop(self) -> None:
252
291
  """Stop the module."""
@@ -0,0 +1,139 @@
1
+ """Background module manager."""
2
+
3
+ import abc
4
+ from collections.abc import AsyncGenerator, AsyncIterator, Callable, Coroutine
5
+ from contextlib import asynccontextmanager
6
+ from typing import Any, Generic
7
+
8
+ from digitalkin.models import ModuleStatus
9
+ from digitalkin.models.module import InputModelT, OutputModelT, SetupModelT
10
+ from digitalkin.modules._base_module import BaseModule
11
+ from digitalkin.services.services_config import ServicesConfig
12
+ from digitalkin.services.services_models import ServicesMode
13
+
14
+
15
+ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
16
+ """Abstract base class for managing background module jobs."""
17
+
18
+ async def _start(self) -> None:
19
+ """Start the job manager.
20
+
21
+ This method initializes any necessary resources or configurations
22
+ required for the job manager to function.
23
+ """
24
+
25
+ @staticmethod
26
+ async def job_specific_callback(
27
+ callback: Callable[[str, OutputModelT], Coroutine[Any, Any, None]], job_id: str
28
+ ) -> Callable[[OutputModelT], Coroutine[Any, Any, None]]:
29
+ """Generate a job-specific callback function.
30
+
31
+ Args:
32
+ callback: The callback function to be executed when the job completes.
33
+ job_id: The unique identifier of the job.
34
+
35
+ Returns:
36
+ Callable: A wrapped callback function that includes the job ID.
37
+ """
38
+
39
+ def callback_wrapper(output_data: OutputModelT) -> Coroutine[Any, Any, None]:
40
+ """Wrapper for the callback function.
41
+
42
+ Args:
43
+ output_data: The output data produced by the job.
44
+
45
+ Returns:
46
+ Coroutine: The wrapped callback function.
47
+ """
48
+ return callback(job_id, output_data)
49
+
50
+ return callback_wrapper
51
+
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
+ @abc.abstractmethod # type: ignore
73
+ @asynccontextmanager # type: ignore
74
+ async def generate_stream_consumer(self, job_id: str) -> AsyncIterator[AsyncGenerator[dict[str, Any], None]]:
75
+ """Generate a stream consumer for the job's message stream.
76
+
77
+ Args:
78
+ job_id: The unique identifier of the job to filter messages for.
79
+
80
+ Yields:
81
+ dict[str, Any]: The messages from the associated module's stream.
82
+ """
83
+
84
+ @abc.abstractmethod
85
+ async def create_job(
86
+ self,
87
+ input_data: InputModelT,
88
+ setup_data: SetupModelT,
89
+ mission_id: str,
90
+ setup_version_id: str,
91
+ ) -> str:
92
+ """Create and start a new job for the module.
93
+
94
+ Args:
95
+ input_data: The input data required to start the job.
96
+ setup_data: The setup configuration for the module.
97
+ mission_id: The mission ID associated with the job.
98
+ setup_version_id: The setup ID associated with the module.
99
+
100
+ Returns:
101
+ str: The unique identifier (job ID) of the created job.
102
+ """
103
+
104
+ @abc.abstractmethod
105
+ async def stop_module(self, job_id: str) -> bool:
106
+ """Stop a running module job.
107
+
108
+ Args:
109
+ job_id: The unique identifier of the job to stop.
110
+
111
+ Returns:
112
+ bool: True if the job was successfully stopped, False if it does not exist.
113
+ """
114
+
115
+ @abc.abstractmethod
116
+ async def get_module_status(self, job_id: str) -> ModuleStatus | None:
117
+ """Retrieve the status of a module job.
118
+
119
+ Args:
120
+ job_id: The unique identifier of the job.
121
+
122
+ Returns:
123
+ ModuleStatus | None: The status of the job, or None if the job does not exist.
124
+ """
125
+
126
+ @abc.abstractmethod
127
+ async def stop_all_modules(self) -> None:
128
+ """Stop all currently running module jobs.
129
+
130
+ This method ensures that all active jobs are gracefully terminated.
131
+ """
132
+
133
+ @abc.abstractmethod
134
+ async def list_modules(self) -> dict[str, dict[str, Any]]:
135
+ """List all modules along with their statuses.
136
+
137
+ Returns:
138
+ dict[str, dict[str, Any]]: A dictionary containing information about all modules and their statuses.
139
+ """
@@ -0,0 +1,44 @@
1
+ """Job manager models."""
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from digitalkin.modules.job_manager.base_job_manager import BaseJobManager
8
+
9
+
10
+ class StreamCodeModel(BaseModel):
11
+ """Typed error/code model."""
12
+
13
+ code: str
14
+
15
+
16
+ class JobManagerMode(Enum):
17
+ """Job manager mode."""
18
+
19
+ SINGLE = "single"
20
+ TASKIQ = "taskiq"
21
+
22
+ def __str__(self) -> str:
23
+ """Get the string representation of the job manager mode.
24
+
25
+ Returns:
26
+ str: job manager mode name.
27
+ """
28
+ return self.value
29
+
30
+ def get_manager_class(self) -> type[BaseJobManager]:
31
+ """Get the job manager class based on the mode.
32
+
33
+ Returns:
34
+ type: The job manager class.
35
+ """
36
+ match self:
37
+ case JobManagerMode.SINGLE:
38
+ from digitalkin.modules.job_manager.single_job_manager import SingleJobManager # noqa: PLC0415
39
+
40
+ return SingleJobManager
41
+ case JobManagerMode.TASKIQ:
42
+ from digitalkin.modules.job_manager.taskiq_job_manager import TaskiqJobManager # noqa: PLC0415
43
+
44
+ return TaskiqJobManager