digitalkin 0.3.3.dev5__tar.gz → 0.3.3.dev7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/PKG-INFO +1 -1
  2. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/pyproject.toml +1 -1
  3. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/__version__.py +1 -1
  4. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/job_manager/base_job_manager.py +6 -0
  5. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/job_manager/taskiq_broker.py +61 -0
  6. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/job_manager/taskiq_job_manager.py +207 -94
  7. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/task_manager/task_session.py +2 -0
  8. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/module_server.py +10 -0
  9. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin.egg-info/PKG-INFO +1 -1
  10. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/LICENSE +0 -0
  11. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/README.md +0 -0
  12. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/__init__.py +0 -0
  13. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/mock/__init__.py +0 -0
  14. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/mock/mock_pb2.py +0 -0
  15. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
  16. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/server_async_insecure.py +0 -0
  17. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/server_async_secure.py +0 -0
  18. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/server_sync_insecure.py +0 -0
  19. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/base_server/server_sync_secure.py +0 -0
  20. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/modules/__init__.py +0 -0
  21. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/modules/archetype_with_tools_module.py +0 -0
  22. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/modules/cpu_intensive_module.py +0 -0
  23. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/modules/dynamic_setup_module.py +0 -0
  24. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/modules/minimal_llm_module.py +0 -0
  25. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/modules/text_transform_module.py +0 -0
  26. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
  27. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
  28. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
  29. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
  30. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
  31. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/monitoring/tests/test_metrics.py +0 -0
  32. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/services/filesystem_module.py +0 -0
  33. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/examples/services/storage_module.py +0 -0
  34. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/setup.cfg +0 -0
  35. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/__init__.py +0 -0
  36. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/__init__.py +0 -0
  37. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/common/__init__.py +0 -0
  38. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/common/factories.py +0 -0
  39. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/job_manager/__init__.py +0 -0
  40. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/job_manager/single_job_manager.py +0 -0
  41. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/profiling/__init__.py +0 -0
  42. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/profiling/asyncio_monitor.py +0 -0
  43. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/profiling/task_profiler.py +0 -0
  44. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/task_manager/__init__.py +0 -0
  45. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/task_manager/base_task_manager.py +0 -0
  46. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/task_manager/local_task_manager.py +0 -0
  47. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
  48. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/core/task_manager/task_executor.py +0 -0
  49. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/__init__.py +0 -0
  50. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/_base_server.py +0 -0
  51. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/module_servicer.py +0 -0
  52. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
  53. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/utils/exceptions.py +0 -0
  54. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +0 -0
  55. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +0 -0
  56. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
  57. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/logger.py +0 -0
  58. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/__init__.py +0 -0
  59. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/base_mixin.py +0 -0
  60. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/callback_mixin.py +0 -0
  61. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/chat_history_mixin.py +0 -0
  62. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/cost_mixin.py +0 -0
  63. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/file_history_mixin.py +0 -0
  64. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
  65. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/logger_mixin.py +0 -0
  66. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/mixins/storage_mixin.py +0 -0
  67. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/__init__.py +0 -0
  68. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/core/__init__.py +0 -0
  69. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/core/job_manager_models.py +0 -0
  70. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/core/task_monitor.py +0 -0
  71. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
  72. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/grpc_servers/models.py +0 -0
  73. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/grpc_servers/types.py +0 -0
  74. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/__init__.py +0 -0
  75. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/base_types.py +0 -0
  76. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/module.py +0 -0
  77. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/module_context.py +0 -0
  78. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/module_types.py +0 -0
  79. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/request_metadata.py +0 -0
  80. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/select_schema.py +0 -0
  81. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/setup_types.py +0 -0
  82. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/tool_cache.py +0 -0
  83. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/tool_reference.py +0 -0
  84. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/module/utility.py +0 -0
  85. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/services/__init__.py +0 -0
  86. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/services/cost.py +0 -0
  87. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/services/registry.py +0 -0
  88. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/models/services/storage.py +0 -0
  89. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/__init__.py +0 -0
  90. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/_base_module.py +0 -0
  91. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/archetype_module.py +0 -0
  92. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/tool_module.py +0 -0
  93. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/trigger_handler.py +0 -0
  94. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/triggers/__init__.py +0 -0
  95. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
  96. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
  97. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
  98. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/py.typed +0 -0
  99. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/__init__.py +0 -0
  100. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/agent/__init__.py +0 -0
  101. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/agent/agent_strategy.py +0 -0
  102. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/agent/default_agent.py +0 -0
  103. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/base_strategy.py +0 -0
  104. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/communication/__init__.py +0 -0
  105. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/communication/communication_strategy.py +0 -0
  106. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/communication/default_communication.py +0 -0
  107. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/communication/grpc_communication.py +0 -0
  108. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/cost/__init__.py +0 -0
  109. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/cost/cost_strategy.py +0 -0
  110. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/cost/default_cost.py +0 -0
  111. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/cost/grpc_cost.py +0 -0
  112. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/filesystem/__init__.py +0 -0
  113. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
  114. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
  115. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
  116. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/identity/__init__.py +0 -0
  117. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/identity/default_identity.py +0 -0
  118. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/identity/identity_strategy.py +0 -0
  119. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/registry/__init__.py +0 -0
  120. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/registry/default_registry.py +0 -0
  121. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/registry/exceptions.py +0 -0
  122. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/registry/grpc_registry.py +0 -0
  123. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/registry/registry_models.py +0 -0
  124. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/registry/registry_strategy.py +0 -0
  125. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/services_config.py +0 -0
  126. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/services_models.py +0 -0
  127. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/setup/__init__.py +0 -0
  128. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/setup/default_setup.py +0 -0
  129. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/setup/grpc_setup.py +0 -0
  130. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/setup/setup_strategy.py +0 -0
  131. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/snapshot/__init__.py +0 -0
  132. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/snapshot/default_snapshot.py +0 -0
  133. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/snapshot/snapshot_strategy.py +0 -0
  134. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/storage/__init__.py +0 -0
  135. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/storage/default_storage.py +0 -0
  136. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/storage/grpc_storage.py +0 -0
  137. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/storage/storage_strategy.py +0 -0
  138. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/task_manager/__init__.py +0 -0
  139. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
  140. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/task_manager/grpc_task_manager.py +0 -0
  141. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
  142. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/user_profile/__init__.py +0 -0
  143. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
  144. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
  145. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
  146. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/__init__.py +0 -0
  147. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/arg_parser.py +0 -0
  148. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/conditional_schema.py +0 -0
  149. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/development_mode_action.py +0 -0
  150. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/dynamic_schema.py +0 -0
  151. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/llm_ready_schema.py +0 -0
  152. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/package_discover.py +0 -0
  153. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/proto_utils.py +0 -0
  154. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin/utils/schema_splitter.py +0 -0
  155. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin.egg-info/SOURCES.txt +0 -0
  156. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin.egg-info/dependency_links.txt +0 -0
  157. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin.egg-info/requires.txt +0 -0
  158. {digitalkin-0.3.3.dev5 → digitalkin-0.3.3.dev7}/src/digitalkin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.3.3.dev5
3
+ Version: 0.3.3.dev7
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -34,7 +34,7 @@
34
34
  "grpcio-status==1.78.0",
35
35
  "pydantic==2.12.5",
36
36
  ]
37
- version = "0.3.3.dev5"
37
+ version = "0.3.3.dev7"
38
38
 
39
39
  [project.optional-dependencies]
40
40
  profiling = [
@@ -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.3.3.dev5"
8
+ __version__ = "0.3.3.dev7"
@@ -130,6 +130,12 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, OutputModelT, SetupModelT]):
130
130
  required for the job manager to function.
131
131
  """
132
132
 
133
+ async def stop(self) -> None:
134
+ """Stop the job manager and clean up resources.
135
+
136
+ Default no-op. Subclasses with external connections override.
137
+ """
138
+
133
139
  @staticmethod
134
140
  async def job_specific_callback(
135
141
  callback: Callable[[str, DataModel | ModuleCodeModel], Coroutine[Any, Any, None]],
@@ -11,8 +11,10 @@ from rstream import Producer
11
11
  from rstream.exceptions import PreconditionFailed
12
12
  from taskiq import Context, TaskiqDepends, TaskiqMessage
13
13
  from taskiq.abc.formatter import TaskiqFormatter
14
+ from taskiq.abc.middleware import TaskiqMiddleware
14
15
  from taskiq.compat import model_validate
15
16
  from taskiq.message import BrokerMessage
17
+ from taskiq.result import TaskiqResult
16
18
  from taskiq_aio_pika import AioPikaBroker
17
19
 
18
20
  from digitalkin.core.common import ModuleFactory
@@ -175,6 +177,11 @@ class TaskiqBrokerConfig:
175
177
  startup=[TaskiqBrokerConfig.init_rstream],
176
178
  )
177
179
  broker.formatter = PickleFormatter()
180
+ redis_url = os.environ.get("DIGITALKIN_TASKIQ_RESULT_BACKEND_URL")
181
+ if redis_url:
182
+ from taskiq_redis import RedisAsyncResultBackend
183
+
184
+ broker.with_result_backend(RedisAsyncResultBackend(redis_url))
178
185
  return broker
179
186
 
180
187
  @staticmethod
@@ -224,9 +231,54 @@ class TaskiqBrokerConfig:
224
231
  await RSTREAM_PRODUCER.send(stream=TaskiqBrokerConfig.STREAM, message=body)
225
232
 
226
233
 
234
+ class TaskiqLifecycleMiddleware(TaskiqMiddleware):
235
+ """Lifecycle middleware for structured logging and safety-net EndOfStreamOutput."""
236
+
237
+ async def pre_execute(self, message: TaskiqMessage) -> TaskiqMessage: # noqa: PLR6301
238
+ """Log task start.
239
+
240
+ Returns:
241
+ The unmodified message.
242
+ """
243
+ logger.info("Taskiq task starting: %s (task_name=%s)", message.task_id, message.task_name)
244
+ return message
245
+
246
+ async def post_execute(self, message: TaskiqMessage, result: TaskiqResult) -> None: # noqa: PLR6301
247
+ """Log task completion."""
248
+ log_fn = logger.info if not result.is_err else logger.error
249
+ log_fn(
250
+ "Taskiq task finished: %s (task_name=%s, is_err=%s, exec_time=%.3fs)",
251
+ message.task_id,
252
+ message.task_name,
253
+ result.is_err,
254
+ result.execution_time,
255
+ )
256
+
257
+ async def on_error( # noqa: PLR6301
258
+ self,
259
+ message: TaskiqMessage,
260
+ result: TaskiqResult, # noqa: ARG002
261
+ exception: BaseException,
262
+ ) -> None:
263
+ """Safety net: send EndOfStreamOutput if worker task failed to."""
264
+ logger.error("Taskiq task error: %s (task_name=%s, error=%s)", message.task_id, message.task_name, exception)
265
+ try:
266
+ await TaskiqBrokerConfig.send_message_to_stream(
267
+ message.task_id,
268
+ ModuleCodeModel(code="WorkerCrash", short_description="Middleware safety net", message=str(exception)),
269
+ )
270
+ await TaskiqBrokerConfig.send_message_to_stream(
271
+ message.task_id,
272
+ DataModel(root=EndOfStreamOutput()),
273
+ )
274
+ except Exception:
275
+ logger.exception("Middleware safety net failed for %s", message.task_id)
276
+
277
+
227
278
  # Module-level globals required by Taskiq framework (decorator needs broker at import time)
228
279
  RSTREAM_PRODUCER = TaskiqBrokerConfig.define_producer()
229
280
  TASKIQ_BROKER = TaskiqBrokerConfig.define_broker()
281
+ TASKIQ_BROKER.add_middlewares(TaskiqLifecycleMiddleware())
230
282
 
231
283
 
232
284
  @TASKIQ_BROKER.task(task_name="__discarded__")
@@ -367,6 +419,7 @@ async def run_config_module(
367
419
  services_mode: ServicesMode,
368
420
  config_setup_data: dict,
369
421
  request_metadata: dict[str, str] | None = None,
422
+ registry_config: dict[str, Any] | None = None,
370
423
  context: Context = TaskiqDepends(),
371
424
  ) -> None:
372
425
  """TaskIQ task allowing a module to compute in the background asynchronously.
@@ -379,9 +432,17 @@ async def run_config_module(
379
432
  services_mode: ServicesMode,
380
433
  config_setup_data: dict,
381
434
  request_metadata: gRPC request metadata (headers) to forward to the module.
435
+ registry_config: Registry config (client_config) forwarded from the main process.
382
436
  context: Allow TaskIQ context access
383
437
  """
384
438
  logger.info("Starting config module with services_mode: %s", services_mode)
439
+
440
+ # Restore registry config lost during pickle (worker re-imports class without runtime mutations)
441
+ if registry_config is not None:
442
+ if "services_config_params" not in module_class.__dict__:
443
+ module_class.services_config_params = dict(module_class.services_config_params)
444
+ module_class.services_config_params["registry"] = registry_config
445
+
385
446
  services_config = ServicesConfig(
386
447
  services_config_strategies=module_class.services_config_strategies,
387
448
  services_config_params=module_class.services_config_params,
@@ -9,6 +9,7 @@ except ImportError:
9
9
 
10
10
  import asyncio
11
11
  import contextlib
12
+ import datetime
12
13
  import json
13
14
  import os
14
15
  from collections.abc import AsyncGenerator, AsyncIterator
@@ -37,6 +38,9 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
37
38
  """Taskiq job manager for running modules in Taskiq tasks."""
38
39
 
39
40
  services_mode: ServicesMode
41
+ stream_consumer: Consumer
42
+ stream_consumer_task: asyncio.Task[None]
43
+ _reaper_task: asyncio.Task[None]
40
44
 
41
45
  @staticmethod
42
46
  async def _on_consumer_closed(reason: Any) -> None:
@@ -86,8 +90,95 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
86
90
  job_id = data.get("job_id")
87
91
  if not job_id:
88
92
  return
93
+ output_data = data.get("output_data")
89
94
  if queue := self.job_queues.get(job_id):
90
- await queue.put(data.get("output_data"))
95
+ await queue.put(output_data)
96
+
97
+ # Bridge session status from RStream terminal markers
98
+ session = self.tasks_sessions.get(job_id)
99
+ if session is None or not isinstance(output_data, dict):
100
+ return
101
+ if "code" in output_data:
102
+ if session.status not in {"cancelled", "failed"}:
103
+ session.status = "failed"
104
+ logger.info("Job %s marked failed from RStream error (code=%s)", job_id, output_data.get("code"))
105
+ elif isinstance(output_data.get("root"), dict) and output_data["root"].get("protocol") == "end_of_stream":
106
+ if session.status not in {"cancelled", "failed"}:
107
+ session.status = "completed"
108
+ logger.info("Job %s marked completed from RStream end_of_stream", job_id)
109
+ session.close_stream()
110
+
111
+ async def _run_consumer_with_restart(self) -> None:
112
+ """Run the RStream consumer with automatic restart on failure.
113
+
114
+ Raises:
115
+ CancelledError: If the task is cancelled.
116
+ """
117
+ max_retries = int(os.environ.get("DIGITALKIN_RSTREAM_MAX_RETRIES", "10"))
118
+ base_delay = 1.0
119
+ max_delay = 60.0
120
+ attempt = 0
121
+
122
+ while True:
123
+ try:
124
+ await self.stream_consumer.run()
125
+ break # Normal exit (consumer closed gracefully)
126
+ except asyncio.CancelledError:
127
+ raise
128
+ except Exception:
129
+ attempt += 1
130
+ if attempt > max_retries:
131
+ logger.exception("Stream consumer failed after %d retries, giving up", max_retries)
132
+ for session in list(self.tasks_sessions.values()):
133
+ if session.status == "pending":
134
+ session.status = "failed"
135
+ session.close_stream()
136
+ break
137
+ delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
138
+ logger.exception(
139
+ "Stream consumer failed (attempt %d/%d), restarting in %.1fs", attempt, max_retries, delay
140
+ )
141
+ await asyncio.sleep(delay)
142
+ # Reconnect
143
+ self.stream_consumer = self._define_consumer()
144
+ await self.stream_consumer.create_stream(
145
+ TaskiqBrokerConfig.STREAM,
146
+ exists_ok=True,
147
+ arguments={"max-length-bytes": TaskiqBrokerConfig.STREAM_RETENTION},
148
+ )
149
+ await self.stream_consumer.start()
150
+ await self.stream_consumer.subscribe(
151
+ stream=TaskiqBrokerConfig.STREAM,
152
+ subscriber_name=f"""subscriber_{os.environ.get("SERVER_NAME", "module_servicer")}""",
153
+ callback=self._on_message, # type: ignore[arg-type]
154
+ offset_specification=ConsumerOffsetSpecification(OffsetType.LAST),
155
+ initial_credit=int(os.environ.get("DIGITALKIN_RSTREAM_INITIAL_CREDIT", "50")),
156
+ )
157
+ logger.info("Stream consumer reconnected (attempt %d)", attempt)
158
+
159
+ async def _reap_orphan_sessions(self) -> None:
160
+ """Mark sessions stuck in pending beyond timeout as failed.
161
+
162
+ Handles hard worker crashes where no EndOfStreamOutput arrives.
163
+ """
164
+ orphan_timeout = float(os.environ.get("DIGITALKIN_ORPHAN_SESSION_TIMEOUT", "600.0"))
165
+ check_interval = float(os.environ.get("DIGITALKIN_ORPHAN_CHECK_INTERVAL", "60.0"))
166
+
167
+ while True:
168
+ try:
169
+ await asyncio.sleep(check_interval)
170
+ except asyncio.CancelledError:
171
+ return
172
+ now = datetime.datetime.now(datetime.timezone.utc)
173
+ for task_id, session in list(self.tasks_sessions.items()):
174
+ if session.status != "pending":
175
+ continue
176
+ elapsed = (now - session.created_at).total_seconds()
177
+ if elapsed > orphan_timeout:
178
+ logger.warning("Orphan session: %s (pending %.0fs)", task_id, elapsed)
179
+ session.status = "failed"
180
+ session.close_stream()
181
+ await self._task_manager._cleanup_task(task_id, session.mission_id) # noqa: SLF001
91
182
 
92
183
  async def start(self) -> None:
93
184
  """Start the TaskiqJobManager (no-op for external connections)."""
@@ -116,37 +207,41 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
116
207
  initial_credit=int(os.environ.get("DIGITALKIN_RSTREAM_INITIAL_CREDIT", "50")),
117
208
  )
118
209
 
119
- # Wrap the consumer task with error handling
120
- async def run_consumer_with_error_handling() -> None:
121
- try:
122
- await self.stream_consumer.run()
123
- except asyncio.CancelledError:
124
- logger.debug("Stream consumer task cancelled")
125
- raise
126
- except Exception:
127
- logger.exception("Stream consumer task failed")
128
- raise
129
-
130
210
  self.stream_consumer_task = asyncio.create_task(
131
- run_consumer_with_error_handling(),
211
+ self._run_consumer_with_restart(),
132
212
  name="stream_consumer_task",
133
213
  )
134
214
 
135
- async def _stop(self) -> None:
136
- """Stop the TaskiqJobManager and clean up all resources."""
137
- # Signal the consumer to stop
215
+ self._reaper_task = asyncio.create_task(self._reap_orphan_sessions(), name="orphan_session_reaper")
216
+
217
+ async def stop(self) -> None:
218
+ """Stop the TaskiqJobManager, cancel workers, and clean up all resources."""
219
+ # 1. Cancel reaper
220
+ self._reaper_task.cancel()
221
+ with contextlib.suppress(asyncio.CancelledError):
222
+ await self._reaper_task
223
+
224
+ # 2. Cancel all running modules (sends cancel signals to workers)
225
+ await self.stop_all_modules()
226
+
227
+ # 3. Clean remaining sessions (releases semaphore slots)
228
+ for task_id in list(self.tasks_sessions.keys()):
229
+ session = self.tasks_sessions.get(task_id)
230
+ if session is not None:
231
+ await self._task_manager._cleanup_task(task_id, session.mission_id) # noqa: SLF001
232
+
233
+ # 4. Close RStream consumer
138
234
  await self.stream_consumer.close()
139
- # Cancel the background task
140
235
  self.stream_consumer_task.cancel()
141
236
  with contextlib.suppress(asyncio.CancelledError):
142
237
  await self.stream_consumer_task
143
238
 
144
- # Clean up job queues
239
+ # 5. Clear job queues
145
240
  queue_count = len(self.job_queues)
146
241
  self.job_queues.clear()
147
- logger.info("TaskiqJobManager: Cleared %d job queues", queue_count)
242
+ logger.info("TaskiqJobManager stopped: cleared %d queues", queue_count)
148
243
 
149
- # Call global cleanup for producer and broker
244
+ # 6. Close producer and broker
150
245
  await TaskiqBrokerConfig.cleanup_global_resources()
151
246
 
152
247
  def __init__(
@@ -192,11 +287,11 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
192
287
  Raises:
193
288
  asyncio.TimeoutError: If waiting for the setup response times out.
194
289
  """
195
- queue = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
196
- self.job_queues[job_id] = queue
290
+ if job_id not in self.job_queues:
291
+ self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
292
+ queue = self.job_queues[job_id]
197
293
 
198
294
  try:
199
- # Add timeout to prevent indefinite blocking
200
295
  item = await asyncio.wait_for(queue.get(), timeout=self._config_setup_timeout)
201
296
  except asyncio.TimeoutError:
202
297
  logger.error(
@@ -207,8 +302,9 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
207
302
  queue.task_done()
208
303
  return item
209
304
  finally:
210
- logger.info("generate_config_setup_module_response: job_id=%s: %s", job_id, self.job_queues[job_id].empty())
211
305
  self.job_queues.pop(job_id, None)
306
+ if (session := self.tasks_sessions.get(job_id)) is not None:
307
+ await self._task_manager._cleanup_task(job_id, session.mission_id) # noqa: SLF001
212
308
 
213
309
  async def create_config_setup_instance_job(
214
310
  self,
@@ -245,6 +341,8 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
245
341
  raise TypeError(msg)
246
342
 
247
343
  # Submit task to Taskiq
344
+ registry_config = self.module_class.services_config_params.get("registry")
345
+
248
346
  running_task: AsyncTaskiqTask[Any] = await task.kiq(
249
347
  mission_id,
250
348
  setup_id,
@@ -253,37 +351,46 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
253
351
  self.services_mode,
254
352
  config_setup_data.model_dump(mode="json"), # SetupModelT generic bound to BaseModel # type: ignore
255
353
  request_metadata,
354
+ registry_config,
256
355
  )
257
356
 
258
357
  job_id = running_task.task_id
259
358
 
260
- # Create module instance for metadata
261
- module = self.module_class(
262
- job_id,
263
- mission_id=mission_id,
264
- setup_id=setup_id,
265
- setup_version_id=setup_version_id,
266
- request_metadata=request_metadata,
267
- )
359
+ # Pre-create queue to avoid message drop race
360
+ self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
268
361
 
269
- # Register task in TaskManager (remote mode)
270
- async def _dummy_coro() -> None:
271
- """Dummy coroutine - actual execution happens in worker."""
362
+ try:
363
+ # Create module instance for metadata
364
+ module = self.module_class(
365
+ job_id,
366
+ mission_id=mission_id,
367
+ setup_id=setup_id,
368
+ setup_version_id=setup_version_id,
369
+ request_metadata=request_metadata,
370
+ )
272
371
 
273
- await self.create_task(
274
- job_id,
275
- mission_id,
276
- module,
277
- _dummy_coro(),
278
- )
372
+ # Register task in TaskManager (remote mode)
373
+ async def _dummy_coro() -> None:
374
+ """Dummy coroutine - actual execution happens in worker."""
375
+
376
+ await self.create_task(
377
+ job_id,
378
+ mission_id,
379
+ module,
380
+ _dummy_coro(),
381
+ )
382
+ except Exception:
383
+ self.job_queues.pop(job_id, None)
384
+ raise
279
385
 
280
- logger.info("Registered config task: %s, waiting for initial result", job_id)
281
- result = await running_task.wait_result(timeout=10)
282
- logger.info("Job %s with data %s", job_id, result)
386
+ logger.info("Registered config task: %s", job_id)
387
+ if os.environ.get("DIGITALKIN_TASKIQ_RESULT_BACKEND_URL"):
388
+ result = await running_task.wait_result(timeout=10)
389
+ logger.debug("Job %s config result: %s", job_id, result)
283
390
  return job_id
284
391
 
285
392
  @asynccontextmanager
286
- async def generate_stream_consumer(self, job_id: str) -> AsyncIterator[AsyncGenerator[dict[str, Any], None]]:
393
+ async def generate_stream_consumer(self, job_id: str) -> AsyncIterator[AsyncGenerator[dict[str, Any], None]]: # noqa: C901, PLR0915
287
394
  """Generate a stream consumer for the RStream stream.
288
395
 
289
396
  Args:
@@ -292,10 +399,11 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
292
399
  Yields:
293
400
  messages: The stream messages from the associated module.
294
401
  """
295
- queue = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
296
- self.job_queues[job_id] = queue
402
+ if job_id not in self.job_queues:
403
+ self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
404
+ queue = self.job_queues[job_id]
297
405
 
298
- async def _stream() -> AsyncGenerator[dict[str, Any], Any]:
406
+ async def _stream() -> AsyncGenerator[dict[str, Any], Any]: # noqa: C901
299
407
  """Generate the stream with batch-drain optimization.
300
408
 
301
409
  Yields:
@@ -349,16 +457,22 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
349
457
  logger.info("Job %s no longer registered, ending stream", job_id)
350
458
  break
351
459
 
352
- status = await self.get_module_status(job_id)
460
+ session = self.tasks_sessions[job_id]
461
+ if session.stream_closed:
462
+ logger.info("Job %s stream closed, draining queue and ending stream", job_id)
463
+ while not queue.empty():
464
+ item = queue.get_nowait()
465
+ queue.task_done()
466
+ yield item
467
+ break
353
468
 
354
- if status in {"cancelled", "failed"}:
469
+ status = await self.get_module_status(job_id)
470
+ if status in {"cancelled", "failed", "completed"}:
355
471
  logger.info("Job %s has terminal status %s, draining queue and ending stream", job_id, status)
356
-
357
472
  while not queue.empty():
358
473
  item = queue.get_nowait()
359
474
  queue.task_done()
360
475
  yield item
361
-
362
476
  break
363
477
 
364
478
  try:
@@ -414,29 +528,37 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
414
528
  )
415
529
  job_id = running_task.task_id
416
530
 
417
- # Create module instance for metadata
418
- module = self.module_class(
419
- job_id,
420
- mission_id=mission_id,
421
- setup_id=setup_id,
422
- setup_version_id=setup_version_id,
423
- request_metadata=request_metadata,
424
- )
531
+ # Pre-create queue to avoid message drop race
532
+ self.job_queues[job_id] = QueueFactory.create_bounded_queue(maxsize=self.max_queue_size)
425
533
 
426
- # Register task in TaskManager (remote mode)
427
- async def _dummy_coro() -> None:
428
- """Dummy coroutine - actual execution happens in worker."""
534
+ try:
535
+ # Create module instance for metadata
536
+ module = self.module_class(
537
+ job_id,
538
+ mission_id=mission_id,
539
+ setup_id=setup_id,
540
+ setup_version_id=setup_version_id,
541
+ request_metadata=request_metadata,
542
+ )
429
543
 
430
- await self.create_task(
431
- job_id,
432
- mission_id,
433
- module,
434
- _dummy_coro(),
435
- )
544
+ # Register task in TaskManager (remote mode)
545
+ async def _dummy_coro() -> None:
546
+ """Dummy coroutine - actual execution happens in worker."""
547
+
548
+ await self.create_task(
549
+ job_id,
550
+ mission_id,
551
+ module,
552
+ _dummy_coro(),
553
+ )
554
+ except Exception:
555
+ self.job_queues.pop(job_id, None)
556
+ raise
436
557
 
437
- logger.info("Registered remote task: %s, waiting for initial result", job_id)
438
- result = await running_task.wait_result(timeout=10)
439
- logger.debug("Job %s with data %s", job_id, result)
558
+ logger.info("Registered remote task: %s", job_id)
559
+ if os.environ.get("DIGITALKIN_TASKIQ_RESULT_BACKEND_URL"):
560
+ result = await running_task.wait_result(timeout=10)
561
+ logger.debug("Job %s result: %s", job_id, result)
440
562
  return job_id
441
563
 
442
564
  async def get_module_status(self, job_id: str) -> str:
@@ -455,37 +577,28 @@ class TaskiqJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
455
577
  return session.status
456
578
 
457
579
  async def wait_for_completion(self, job_id: str, max_wait: float = 600.0) -> None:
458
- """Wait for a task to complete by polling its status.
580
+ """Wait for a task to complete via stream-closed event.
459
581
 
460
- Uses adaptive polling: starts at 50ms for fast jobs, doubles up to 500ms
461
- for long-running tasks to reduce CPU overhead while maintaining low latency.
582
+ Relies on ``_on_message`` setting ``_stream_closed`` when
583
+ ``end_of_stream`` arrives from RStream. Falls back to ``max_wait``
584
+ timeout for crash scenarios.
462
585
 
463
586
  Args:
464
587
  job_id: The unique identifier of the job to wait for.
465
588
  max_wait: Maximum time in seconds to wait before giving up.
466
589
 
467
590
  Raises:
468
- KeyError: If the job_id is not found in tasks_sessions.
469
591
  asyncio.TimeoutError: If max_wait is exceeded.
470
592
  """
471
- if job_id not in self.tasks_sessions:
472
- msg = f"Job {job_id} not found"
473
- raise KeyError(msg)
474
-
475
- terminal_states = {"completed", "failed", "cancelled"}
476
- poll_interval = 0.05
477
- elapsed = 0.0
478
- while True:
479
- session = self.tasks_sessions.get(job_id)
480
- if session is None or session.status in terminal_states:
481
- logger.debug("Job %s reached terminal state: %s", job_id, session.status if session else "removed")
482
- break
483
- if elapsed >= max_wait:
484
- logger.error("Job %s: max wait time (%.1fs) exceeded, giving up", job_id, max_wait)
485
- raise asyncio.TimeoutError
486
- await asyncio.sleep(poll_interval)
487
- elapsed += poll_interval
488
- poll_interval = min(poll_interval * 2, 0.5)
593
+ session = self.tasks_sessions.get(job_id)
594
+ if session is None:
595
+ return
596
+ try:
597
+ await asyncio.wait_for(session._stream_closed.wait(), timeout=max_wait) # noqa: SLF001
598
+ except asyncio.TimeoutError:
599
+ logger.error("Job %s: max wait time (%.1fs) exceeded", job_id, max_wait)
600
+ raise
601
+ logger.debug("Job %s: stream closed, completion detected (status=%s)", job_id, session.status)
489
602
 
490
603
  async def stop_module(self, job_id: str) -> bool:
491
604
  """Stop a running module using TaskManager.
@@ -31,6 +31,7 @@ class TaskSession:
31
31
  task_id: str
32
32
  mission_id: str
33
33
 
34
+ created_at: datetime.datetime
34
35
  started_at: datetime.datetime | None
35
36
  completed_at: datetime.datetime | None
36
37
 
@@ -73,6 +74,7 @@ class TaskSession:
73
74
  self.task_id = task_id
74
75
  self.mission_id = mission_id
75
76
 
77
+ self.created_at = datetime.datetime.now(datetime.timezone.utc)
76
78
  self.started_at = None
77
79
  self.completed_at = None
78
80
 
@@ -155,6 +155,16 @@ class ModuleServer(BaseServer):
155
155
  except Exception:
156
156
  logger.exception("Failed to shutdown module servicer resources")
157
157
 
158
+ try:
159
+ await self.module_servicer.job_manager.stop_all_modules()
160
+ except Exception:
161
+ logger.exception("Failed to stop all modules during shutdown")
162
+
163
+ try:
164
+ await self.module_servicer.job_manager.stop()
165
+ except Exception:
166
+ logger.exception("Failed to stop job manager during shutdown")
167
+
158
168
  # Close server-level registry channel
159
169
  if isinstance(self.registry, GrpcRegistry):
160
170
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.3.3.dev5
3
+ Version: 0.3.3.dev7
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
File without changes