sunholo 0.145.0__tar.gz → 0.145.3__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 (206) hide show
  1. {sunholo-0.145.0/src/sunholo.egg-info → sunholo-0.145.3}/PKG-INFO +2 -2
  2. {sunholo-0.145.0 → sunholo-0.145.3}/pyproject.toml +3 -3
  3. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/invoke/__init__.py +1 -1
  4. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/invoke/async_task_runner.py +30 -5
  5. {sunholo-0.145.0 → sunholo-0.145.3/src/sunholo.egg-info}/PKG-INFO +2 -2
  6. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo.egg-info/SOURCES.txt +2 -3
  7. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo.egg-info/requires.txt +1 -0
  8. sunholo-0.145.3/tests/test_async_task_runner.py +426 -0
  9. sunholo-0.145.0/tests/test_vac_routes_fastapi.py +0 -226
  10. sunholo-0.145.0/tests/test_vac_routes_mcp.py +0 -398
  11. {sunholo-0.145.0 → sunholo-0.145.3}/LICENSE.txt +0 -0
  12. {sunholo-0.145.0 → sunholo-0.145.3}/MANIFEST.in +0 -0
  13. {sunholo-0.145.0 → sunholo-0.145.3}/README.md +0 -0
  14. {sunholo-0.145.0 → sunholo-0.145.3}/setup.cfg +0 -0
  15. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/__init__.py +0 -0
  16. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/a2a/__init__.py +0 -0
  17. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/a2a/agent_card.py +0 -0
  18. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/a2a/task_manager.py +0 -0
  19. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/a2a/vac_a2a_agent.py +0 -0
  20. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/__init__.py +0 -0
  21. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/chat_history.py +0 -0
  22. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/dispatch_to_qa.py +0 -0
  23. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/fastapi/__init__.py +0 -0
  24. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/fastapi/base.py +0 -0
  25. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/fastapi/qna_routes.py +0 -0
  26. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/fastapi/vac_routes.py +0 -0
  27. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/flask/__init__.py +0 -0
  28. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/flask/base.py +0 -0
  29. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/flask/vac_routes.py +0 -0
  30. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/langserve.py +0 -0
  31. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/pubsub.py +0 -0
  32. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/route.py +0 -0
  33. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/special_commands.py +0 -0
  34. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/agents/swagger.py +0 -0
  35. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/archive/__init__.py +0 -0
  36. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/archive/archive.py +0 -0
  37. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/auth/__init__.py +0 -0
  38. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/auth/gcloud.py +0 -0
  39. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/auth/refresh.py +0 -0
  40. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/auth/run.py +0 -0
  41. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/azure/__init__.py +0 -0
  42. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/azure/auth.py +0 -0
  43. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/azure/blobs.py +0 -0
  44. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/azure/event_grid.py +0 -0
  45. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/bots/__init__.py +0 -0
  46. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/bots/discord.py +0 -0
  47. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/bots/github_webhook.py +0 -0
  48. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/bots/webapp.py +0 -0
  49. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/__init__.py +0 -0
  50. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/azure.py +0 -0
  51. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/doc_handling.py +0 -0
  52. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/encode_metadata.py +0 -0
  53. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/images.py +0 -0
  54. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/loaders.py +0 -0
  55. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/message_data.py +0 -0
  56. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/pdfs.py +0 -0
  57. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/process_chunker_data.py +0 -0
  58. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/publish.py +0 -0
  59. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/pubsub.py +0 -0
  60. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/chunker/splitter.py +0 -0
  61. {sunholo-0.145.0/src/sunholo/templates/system_services → sunholo-0.145.3/src/sunholo/cli}/__init__.py +0 -0
  62. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/chat_vac.py +0 -0
  63. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/cli.py +0 -0
  64. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/cli_init.py +0 -0
  65. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/configs.py +0 -0
  66. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/deploy.py +0 -0
  67. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/embedder.py +0 -0
  68. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/merge_texts.py +0 -0
  69. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/run_proxy.py +0 -0
  70. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/sun_rich.py +0 -0
  71. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/swagger.py +0 -0
  72. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/cli/vertex.py +0 -0
  73. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/components/__init__.py +0 -0
  74. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/components/llm.py +0 -0
  75. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/components/retriever.py +0 -0
  76. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/components/vectorstore.py +0 -0
  77. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/custom_logging.py +0 -0
  78. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/__init__.py +0 -0
  79. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/alloydb.py +0 -0
  80. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/alloydb_client.py +0 -0
  81. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/database.py +0 -0
  82. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/lancedb.py +0 -0
  83. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/sql/sb/create_function.sql +0 -0
  84. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/sql/sb/create_function_time.sql +0 -0
  85. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/sql/sb/create_table.sql +0 -0
  86. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  87. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/sql/sb/return_sources.sql +0 -0
  88. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/sql/sb/setup.sql +0 -0
  89. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/static_dbs.py +0 -0
  90. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/database/uuid.py +0 -0
  91. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/discovery_engine/__init__.py +0 -0
  92. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/discovery_engine/chunker_handler.py +0 -0
  93. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/discovery_engine/cli.py +0 -0
  94. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/discovery_engine/create_new.py +0 -0
  95. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  96. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/discovery_engine/get_ai_search_chunks.py +0 -0
  97. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/embedder/__init__.py +0 -0
  98. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/embedder/embed_chunk.py +0 -0
  99. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/embedder/embed_metadata.py +0 -0
  100. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/excel/__init__.py +0 -0
  101. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/excel/plugin.py +0 -0
  102. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/gcs/__init__.py +0 -0
  103. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/gcs/add_file.py +0 -0
  104. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/gcs/download_folder.py +0 -0
  105. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/gcs/download_gcs_text.py +0 -0
  106. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/gcs/download_url.py +0 -0
  107. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/gcs/extract_and_sign.py +0 -0
  108. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/gcs/metadata.py +0 -0
  109. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/genai/__init__.py +0 -0
  110. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/genai/file_handling.py +0 -0
  111. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/genai/genaiv2.py +0 -0
  112. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/genai/images.py +0 -0
  113. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/genai/init.py +0 -0
  114. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/genai/process_funcs_cls.py +0 -0
  115. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/genai/safety.py +0 -0
  116. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/invoke/async_class.py +0 -0
  117. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/invoke/direct_vac_func.py +0 -0
  118. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/invoke/invoke_vac_utils.py +0 -0
  119. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/langchain_types.py +0 -0
  120. {sunholo-0.145.0/src/sunholo/templates/project → sunholo-0.145.3/src/sunholo/langfuse}/__init__.py +0 -0
  121. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/langfuse/callback.py +0 -0
  122. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/langfuse/evals.py +0 -0
  123. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/langfuse/prompts.py +0 -0
  124. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/llamaindex/__init__.py +0 -0
  125. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/llamaindex/get_files.py +0 -0
  126. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/llamaindex/import_files.py +0 -0
  127. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/llamaindex/llamaindex_class.py +0 -0
  128. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/llamaindex/user_history.py +0 -0
  129. {sunholo-0.145.0/src/sunholo/templates/agent/tools → sunholo-0.145.3/src/sunholo/lookup}/__init__.py +0 -0
  130. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/lookup/model_lookup.yaml +0 -0
  131. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/__init__.py +0 -0
  132. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/cli.py +0 -0
  133. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/cli_fastmcp.py +0 -0
  134. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/extensible_mcp_server.py +0 -0
  135. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/mcp_manager.py +0 -0
  136. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/sse_utils.py +0 -0
  137. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/stdio_http_bridge.py +0 -0
  138. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/vac_mcp_server.py +0 -0
  139. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/vac_mcp_server_fastmcp.py +0 -0
  140. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/mcp/vac_tools.py +0 -0
  141. {sunholo-0.145.0/src/sunholo/templates/agent → sunholo-0.145.3/src/sunholo/ollama}/__init__.py +0 -0
  142. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/ollama/ollama_images.py +0 -0
  143. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/pubsub/__init__.py +0 -0
  144. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/pubsub/process_pubsub.py +0 -0
  145. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/pubsub/pubsub_manager.py +0 -0
  146. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/qna/__init__.py +0 -0
  147. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/qna/parsers.py +0 -0
  148. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/qna/retry.py +0 -0
  149. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/senses/__init__.py +0 -0
  150. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/senses/stream_voice.py +0 -0
  151. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/streaming/__init__.py +0 -0
  152. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/streaming/content_buffer.py +0 -0
  153. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/streaming/langserve.py +0 -0
  154. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/streaming/stream_lookup.py +0 -0
  155. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/streaming/streaming.py +0 -0
  156. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/summarise/__init__.py +0 -0
  157. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/summarise/summarise.py +0 -0
  158. {sunholo-0.145.0/src/sunholo/ollama → sunholo-0.145.3/src/sunholo/templates/agent}/__init__.py +0 -0
  159. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/agent/agent_service.py +0 -0
  160. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/agent/app.py +0 -0
  161. {sunholo-0.145.0/src/sunholo/templates/project → sunholo-0.145.3/src/sunholo/templates/agent}/my_log.py +0 -0
  162. {sunholo-0.145.0/src/sunholo/lookup → sunholo-0.145.3/src/sunholo/templates/agent/tools}/__init__.py +0 -0
  163. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/agent/tools/your_agent.py +0 -0
  164. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/agent/vac_service.py +0 -0
  165. {sunholo-0.145.0/src/sunholo/langfuse → sunholo-0.145.3/src/sunholo/templates/project}/__init__.py +0 -0
  166. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/project/app.py +0 -0
  167. {sunholo-0.145.0/src/sunholo/templates/agent → sunholo-0.145.3/src/sunholo/templates/project}/my_log.py +0 -0
  168. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/project/vac_service.py +0 -0
  169. {sunholo-0.145.0/src/sunholo/cli → sunholo-0.145.3/src/sunholo/templates/system_services}/__init__.py +0 -0
  170. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/system_services/app.py +0 -0
  171. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/templates/system_services/my_log.py +0 -0
  172. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/terraform/__init__.py +0 -0
  173. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/terraform/tfvars_editor.py +0 -0
  174. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/tools/__init__.py +0 -0
  175. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/tools/web_browser.py +0 -0
  176. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/__init__.py +0 -0
  177. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/api_key.py +0 -0
  178. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/big_context.py +0 -0
  179. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/config.py +0 -0
  180. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/config_class.py +0 -0
  181. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/config_schema.py +0 -0
  182. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/gcp.py +0 -0
  183. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/gcp_project.py +0 -0
  184. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/mime.py +0 -0
  185. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/parsers.py +0 -0
  186. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/proto_convert.py +0 -0
  187. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/timedelta.py +0 -0
  188. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/user_ids.py +0 -0
  189. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/utils/version.py +0 -0
  190. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/__init__.py +0 -0
  191. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/extensions_call.py +0 -0
  192. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/extensions_class.py +0 -0
  193. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/genai_functions.py +0 -0
  194. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/init.py +0 -0
  195. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/memory_tools.py +0 -0
  196. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/safety.py +0 -0
  197. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo/vertex/type_dict_to_json.py +0 -0
  198. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo.egg-info/dependency_links.txt +0 -0
  199. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo.egg-info/entry_points.txt +0 -0
  200. {sunholo-0.145.0 → sunholo-0.145.3}/src/sunholo.egg-info/top_level.txt +0 -0
  201. {sunholo-0.145.0 → sunholo-0.145.3}/tests/test_async.py +0 -0
  202. {sunholo-0.145.0 → sunholo-0.145.3}/tests/test_async_genai2.py +0 -0
  203. {sunholo-0.145.0 → sunholo-0.145.3}/tests/test_chat_history.py +0 -0
  204. {sunholo-0.145.0 → sunholo-0.145.3}/tests/test_config.py +0 -0
  205. {sunholo-0.145.0 → sunholo-0.145.3}/tests/test_genai2.py +0 -0
  206. {sunholo-0.145.0 → sunholo-0.145.3}/tests/test_unstructured.py +0 -0
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.145.0
3
+ Version: 0.145.3
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0
7
7
  Project-URL: Homepage, https://github.com/sunholo-data/sunholo-py
8
- Project-URL: Download, https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.118.0.tar.gz
9
8
  Keywords: llms,devops,google_cloud_platform
10
9
  Classifier: Development Status :: 3 - Alpha
11
10
  Classifier: Intended Audience :: Developers
@@ -107,6 +106,7 @@ Requires-Dist: azure-storage-blob; extra == "azure"
107
106
  Provides-Extra: cli
108
107
  Requires-Dist: jsonschema>=4.21.1; extra == "cli"
109
108
  Requires-Dist: rich; extra == "cli"
109
+ Requires-Dist: fastmcp; extra == "cli"
110
110
  Provides-Extra: database
111
111
  Requires-Dist: asyncpg; extra == "database"
112
112
  Requires-Dist: supabase; extra == "database"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sunholo"
7
- version = "0.145.0"
7
+ version = "0.145.3"
8
8
  description = "AI DevOps - a package to help deploy GenAI to the Cloud."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -33,7 +33,6 @@ dependencies = [
33
33
 
34
34
  [project.urls]
35
35
  Homepage = "https://github.com/sunholo-data/sunholo-py"
36
- Download = "https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.118.0.tar.gz"
37
36
 
38
37
  [project.scripts]
39
38
  sunholo = "sunholo.cli.cli:main"
@@ -140,7 +139,8 @@ azure = [
140
139
  ]
141
140
  cli = [
142
141
  "jsonschema>=4.21.1",
143
- "rich"
142
+ "rich",
143
+ "fastmcp"
144
144
  ]
145
145
  database = [
146
146
  "asyncpg",
@@ -1,3 +1,3 @@
1
1
  from .invoke_vac_utils import invoke_vac
2
2
  from .direct_vac_func import direct_vac, direct_vac_stream, async_direct_vac, async_direct_vac_stream
3
- from .async_class import AsyncTaskRunner
3
+ from .async_task_runner import AsyncTaskRunner, CallbackContext, TaskConfig
@@ -96,6 +96,13 @@ class AsyncTaskRunner:
96
96
  >>> results = await runner.get_aggregated_results()
97
97
  >>> print(results['results']) # {'fetch_data': 'data from api'}
98
98
 
99
+ # Custom task names for better clarity
100
+ >>> runner = AsyncTaskRunner()
101
+ >>> runner.add_task(fetch_data, "user_api", task_name="fetch_user_data")
102
+ >>> runner.add_task(fetch_data, "posts_api", task_name="fetch_posts")
103
+ >>> results = await runner.get_aggregated_results()
104
+ >>> print(results['results']['fetch_user_data']) # User data
105
+
99
106
  # Silent mode - no console output but still collects results
100
107
  >>> runner = AsyncTaskRunner(verbose=False)
101
108
 
@@ -108,6 +115,7 @@ class AsyncTaskRunner:
108
115
  >>> runner = AsyncTaskRunner(use_default_callbacks=False)
109
116
  """
110
117
  self.tasks = []
118
+ self.task_name_counts = {} # Track task names to ensure uniqueness
111
119
  self.retry_enabled = retry_enabled
112
120
  self.retry_kwargs = retry_kwargs or {}
113
121
  self.timeout = timeout
@@ -209,18 +217,35 @@ class AsyncTaskRunner:
209
217
  func: Callable[..., Any],
210
218
  *args: Any,
211
219
  task_config: Optional[TaskConfig] = None,
220
+ task_name: Optional[str] = None,
212
221
  **kwargs: Any):
213
222
  """
214
223
  Adds a task to the list of tasks to be executed, with optional per-task configuration.
224
+
225
+ Automatically ensures task names are unique by appending a suffix if needed.
215
226
 
216
227
  Args:
217
228
  func: The function to be executed.
218
229
  *args: Positional arguments for the function.
219
230
  task_config: Optional per-task configuration for timeout, retry, and callbacks.
231
+ task_name: Optional custom name for the task. If not provided, uses func.__name__.
220
232
  **kwargs: Keyword arguments for the function.
221
233
  """
222
- log.info(f"Adding task: {func.__name__} with args: {args}, kwargs: {kwargs}, config: {task_config}")
223
- self.tasks.append((func.__name__, func, args, kwargs, task_config))
234
+ # Get base name from task_name or function name
235
+ base_name = task_name if task_name is not None else func.__name__
236
+
237
+ # Ensure uniqueness by adding suffix if needed
238
+ if base_name in self.task_name_counts:
239
+ # Name already exists, increment count and add suffix
240
+ self.task_name_counts[base_name] += 1
241
+ name = f"{base_name}_{self.task_name_counts[base_name]}"
242
+ else:
243
+ # First occurrence of this name
244
+ self.task_name_counts[base_name] = 0
245
+ name = base_name
246
+
247
+ log.info(f"Adding task: {name} with args: {args}, kwargs: {kwargs}, config: {task_config}")
248
+ self.tasks.append((name, func, args, kwargs, task_config))
224
249
 
225
250
  async def run_async_with_callbacks(self) -> AsyncGenerator[CallbackContext, None]:
226
251
  """
@@ -313,10 +338,10 @@ class AsyncTaskRunner:
313
338
 
314
339
  Example:
315
340
  >>> runner = AsyncTaskRunner()
316
- >>> runner.add_task(fetch_data, "api")
317
- >>> runner.add_task(process_data, "raw_data")
341
+ >>> runner.add_task(fetch_data, "api", task_name="api_fetch")
342
+ >>> runner.add_task(process_data, "raw_data", task_name="data_processing")
318
343
  >>> results = await runner.get_aggregated_results()
319
- >>> print(results['results']['fetch_data']) # Access specific result
344
+ >>> print(results['results']['api_fetch']) # Access specific result
320
345
  >>> if results['errors']: # Check for any errors
321
346
  ... print(f"Errors occurred: {results['errors']}")
322
347
  """
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.145.0
3
+ Version: 0.145.3
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0
7
7
  Project-URL: Homepage, https://github.com/sunholo-data/sunholo-py
8
- Project-URL: Download, https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.118.0.tar.gz
9
8
  Keywords: llms,devops,google_cloud_platform
10
9
  Classifier: Development Status :: 3 - Alpha
11
10
  Classifier: Intended Audience :: Developers
@@ -107,6 +106,7 @@ Requires-Dist: azure-storage-blob; extra == "azure"
107
106
  Provides-Extra: cli
108
107
  Requires-Dist: jsonschema>=4.21.1; extra == "cli"
109
108
  Requires-Dist: rich; extra == "cli"
109
+ Requires-Dist: fastmcp; extra == "cli"
110
110
  Provides-Extra: database
111
111
  Requires-Dist: asyncpg; extra == "database"
112
112
  Requires-Dist: supabase; extra == "database"
@@ -195,9 +195,8 @@ src/sunholo/vertex/safety.py
195
195
  src/sunholo/vertex/type_dict_to_json.py
196
196
  tests/test_async.py
197
197
  tests/test_async_genai2.py
198
+ tests/test_async_task_runner.py
198
199
  tests/test_chat_history.py
199
200
  tests/test_config.py
200
201
  tests/test_genai2.py
201
- tests/test_unstructured.py
202
- tests/test_vac_routes_fastapi.py
203
- tests/test_vac_routes_mcp.py
202
+ tests/test_unstructured.py
@@ -86,6 +86,7 @@ azure-storage-blob
86
86
  [cli]
87
87
  jsonschema>=4.21.1
88
88
  rich
89
+ fastmcp
89
90
 
90
91
  [database]
91
92
  asyncpg
@@ -0,0 +1,426 @@
1
+ """
2
+ Tests for AsyncTaskRunner with default callbacks and enhanced features.
3
+ """
4
+
5
+ import asyncio
6
+ import pytest
7
+ from unittest.mock import AsyncMock, MagicMock
8
+ from sunholo.invoke.async_task_runner import AsyncTaskRunner, TaskConfig, CallbackContext
9
+ from tenacity import stop_after_attempt
10
+
11
+
12
+ # Test fixtures - async functions to use in tests
13
+ async def simple_task(value: str):
14
+ """Simple async task that returns a value."""
15
+ await asyncio.sleep(0.1)
16
+ return f"Result: {value}"
17
+
18
+
19
+ async def failing_task(value: str):
20
+ """Task that always fails for testing error handling."""
21
+ await asyncio.sleep(0.1)
22
+ raise ValueError(f"Intentional error: {value}")
23
+
24
+
25
+ async def slow_task(value: str, duration: float = 2.0):
26
+ """Task with configurable duration for timeout testing."""
27
+ await asyncio.sleep(duration)
28
+ return f"Slow result: {value}"
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_default_callbacks_basic():
33
+ """Test that default callbacks populate shared_state correctly."""
34
+ runner = AsyncTaskRunner(verbose=False) # Quiet for tests
35
+
36
+ # Add some tasks
37
+ runner.add_task(simple_task, "test1")
38
+ runner.add_task(simple_task, "test2")
39
+
40
+ # Run and get results
41
+ results = await runner.get_aggregated_results()
42
+
43
+ # Check that default callbacks populated the state
44
+ assert 'results' in results
45
+ assert 'completed' in results
46
+ assert 'started' in results
47
+ assert 'errors' in results
48
+
49
+ # Check task results - with auto-naming, second task gets _1 suffix
50
+ assert 'simple_task' in results['results']
51
+ assert results['results']['simple_task'] == "Result: test1"
52
+ assert 'simple_task_1' in results['results']
53
+ assert results['results']['simple_task_1'] == "Result: test2"
54
+
55
+ # Check completed list - should have unique names
56
+ assert len(results['completed']) == 2
57
+ assert 'simple_task' in results['completed']
58
+ assert 'simple_task_1' in results['completed']
59
+
60
+ # Check started list
61
+ assert len(results['started']) == 2
62
+
63
+ # No errors expected
64
+ assert len(results['errors']) == 0
65
+
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_default_callbacks_with_errors():
69
+ """Test that default callbacks handle errors correctly."""
70
+ runner = AsyncTaskRunner(verbose=False)
71
+
72
+ # Add both successful and failing tasks
73
+ runner.add_task(simple_task, "success")
74
+ runner.add_task(failing_task, "failure")
75
+
76
+ results = await runner.get_aggregated_results()
77
+
78
+ # Check successful task
79
+ assert 'simple_task' in results['results']
80
+ assert results['results']['simple_task'] == "Result: success"
81
+
82
+ # Check error was captured
83
+ assert 'failing_task' in results['errors']
84
+ assert "Intentional error: failure" in results['errors']['failing_task']
85
+
86
+ # Check completed list (only successful task)
87
+ assert 'simple_task' in results['completed']
88
+
89
+ # Both tasks should have started
90
+ assert len(results['started']) == 2
91
+
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_default_callbacks_with_retry():
95
+ """Test that default callbacks track retry attempts."""
96
+ runner = AsyncTaskRunner(
97
+ verbose=False,
98
+ retry_enabled=False # Global default
99
+ )
100
+
101
+ # Add task with retry enabled
102
+ runner.add_task(
103
+ failing_task,
104
+ "retry_test",
105
+ task_config=TaskConfig(
106
+ retry_enabled=True,
107
+ retry_kwargs={'stop': stop_after_attempt(3)}
108
+ )
109
+ )
110
+
111
+ results = await runner.get_aggregated_results()
112
+
113
+ # Check that retries were tracked
114
+ assert 'retries' in results
115
+ assert len(results['retries']) == 2 # Attempts 2 and 3 (not 1)
116
+ assert results['retries'][0] == 'failing_task_attempt_2'
117
+ assert results['retries'][1] == 'failing_task_attempt_3'
118
+
119
+ # Task should have error after all retries
120
+ assert 'failing_task' in results['errors']
121
+
122
+
123
+ @pytest.mark.asyncio
124
+ async def test_custom_callback_override():
125
+ """Test that custom callbacks override defaults correctly."""
126
+ custom_complete_called = []
127
+
128
+ async def custom_on_complete(ctx: CallbackContext):
129
+ """Custom completion handler for testing."""
130
+ custom_complete_called.append(ctx.task_name)
131
+ # Still populate state like default would
132
+ ctx.shared_state['results'][ctx.task_name] = f"CUSTOM: {ctx.result}"
133
+ ctx.shared_state['completed'].append(ctx.task_name)
134
+
135
+ runner = AsyncTaskRunner(
136
+ verbose=False,
137
+ callbacks={'on_task_complete': custom_on_complete}
138
+ # Other callbacks remain as defaults
139
+ )
140
+
141
+ runner.add_task(simple_task, "test")
142
+ results = await runner.get_aggregated_results()
143
+
144
+ # Check custom callback was called
145
+ assert len(custom_complete_called) == 1
146
+ assert custom_complete_called[0] == 'simple_task'
147
+
148
+ # Check custom result format
149
+ assert results['results']['simple_task'] == "CUSTOM: Result: test"
150
+
151
+ # Default callbacks should still work for other events
152
+ assert 'started' in results
153
+ assert 'simple_task' in results['started']
154
+
155
+
156
+ @pytest.mark.asyncio
157
+ async def test_no_default_callbacks():
158
+ """Test that disabling default callbacks works."""
159
+ runner = AsyncTaskRunner(
160
+ use_default_callbacks=False,
161
+ verbose=False
162
+ )
163
+
164
+ runner.add_task(simple_task, "test")
165
+ results = await runner.get_aggregated_results()
166
+
167
+ # State should be initialized but empty (no callbacks to populate it)
168
+ assert results == {
169
+ 'results': {},
170
+ 'errors': {},
171
+ 'completed': [],
172
+ 'started': [],
173
+ 'retries': [],
174
+ 'timed_out': []
175
+ }
176
+
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_per_task_timeout_with_defaults():
180
+ """Test per-task timeout configuration with default callbacks."""
181
+ runner = AsyncTaskRunner(
182
+ timeout=1, # Default 1 second timeout
183
+ verbose=False
184
+ )
185
+
186
+ # This task should timeout with default
187
+ runner.add_task(slow_task, "timeout_task", 2.0) # Takes 2 seconds
188
+
189
+ # This task should complete with custom timeout
190
+ runner.add_task(
191
+ slow_task,
192
+ "complete_task",
193
+ 0.5, # Takes 0.5 seconds
194
+ task_config=TaskConfig(timeout=3) # 3 second timeout
195
+ )
196
+
197
+ results = await runner.get_aggregated_results()
198
+
199
+ # Check that one timed out (first slow_task)
200
+ assert 'timed_out' in results
201
+ assert 'slow_task' in results['timed_out']
202
+
203
+ # Check that timeout was recorded as error
204
+ assert 'slow_task' in results['errors']
205
+ # The default callback stores "Timeout after Xs" or the error might be "Unknown error" if timeout wasn't caught properly
206
+ error_msg = results['errors'].get('slow_task', '').lower()
207
+ assert 'timeout' in error_msg or 'unknown' in error_msg
208
+
209
+ # The one with extended timeout should complete (gets _1 suffix)
210
+ assert 'slow_task_1' in results['results']
211
+ assert results['results']['slow_task_1'] == "Slow result: complete_task"
212
+
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_shared_state_persistence():
216
+ """Test that shared_state is accessible and modifiable across callbacks."""
217
+ shared_state = {
218
+ 'custom_counter': 0,
219
+ 'task_order': []
220
+ }
221
+
222
+ async def counting_callback(ctx: CallbackContext):
223
+ """Callback that increments a counter."""
224
+ ctx.shared_state['custom_counter'] += 1
225
+ ctx.shared_state['task_order'].append(ctx.task_name)
226
+ # Also do the default behavior
227
+ ctx.shared_state.setdefault('results', {})[ctx.task_name] = ctx.result
228
+
229
+ runner = AsyncTaskRunner(
230
+ shared_state=shared_state,
231
+ callbacks={'on_task_complete': counting_callback},
232
+ verbose=False
233
+ )
234
+
235
+ runner.add_task(simple_task, "first")
236
+ runner.add_task(simple_task, "second")
237
+ runner.add_task(simple_task, "third")
238
+
239
+ results = await runner.get_aggregated_results()
240
+
241
+ # Check custom state was maintained
242
+ assert results['custom_counter'] == 3
243
+ assert len(results['task_order']) == 3
244
+ # With auto-naming, tasks are now: simple_task, simple_task_1, simple_task_2
245
+ assert 'simple_task' in results['task_order']
246
+ assert 'simple_task_1' in results['task_order']
247
+ assert 'simple_task_2' in results['task_order']
248
+
249
+ # Default keys should also be present
250
+ assert 'results' in results
251
+ assert 'errors' in results
252
+ assert 'completed' in results
253
+
254
+
255
+ @pytest.mark.asyncio
256
+ async def test_verbose_mode():
257
+ """Test that verbose mode affects shared_state population but not its behavior."""
258
+ # Test verbose=True (default)
259
+ runner_verbose = AsyncTaskRunner(verbose=True)
260
+ runner_verbose.add_task(simple_task, "verbose_test")
261
+ results_verbose = await runner_verbose.get_aggregated_results()
262
+
263
+ # Should have results regardless of verbose mode
264
+ assert 'simple_task' in results_verbose['results']
265
+ assert 'simple_task' in results_verbose['completed']
266
+
267
+ # Test verbose=False - should still work but quietly
268
+ runner_quiet = AsyncTaskRunner(verbose=False)
269
+ runner_quiet.add_task(simple_task, "quiet_test")
270
+ results_quiet = await runner_quiet.get_aggregated_results()
271
+
272
+ # Should have same results structure
273
+ assert 'simple_task' in results_quiet['results']
274
+ assert 'simple_task' in results_quiet['completed']
275
+
276
+ # Both should produce same result structure
277
+ assert set(results_verbose.keys()) == set(results_quiet.keys())
278
+
279
+
280
+ @pytest.mark.asyncio
281
+ async def test_multiple_tasks_same_name():
282
+ """Test behavior when multiple tasks have the same function name."""
283
+ runner = AsyncTaskRunner(verbose=False)
284
+
285
+ # Add multiple tasks with same function
286
+ runner.add_task(simple_task, "first")
287
+ runner.add_task(simple_task, "second")
288
+ runner.add_task(simple_task, "third")
289
+
290
+ results = await runner.get_aggregated_results()
291
+
292
+ # Results dict should have all three with unique names
293
+ assert results['results']['simple_task'] == "Result: first"
294
+ assert results['results']['simple_task_1'] == "Result: second"
295
+ assert results['results']['simple_task_2'] == "Result: third"
296
+
297
+ # Completed list should have all three with unique names
298
+ assert 'simple_task' in results['completed']
299
+ assert 'simple_task_1' in results['completed']
300
+ assert 'simple_task_2' in results['completed']
301
+
302
+ # Started list should have all three with unique names
303
+ assert 'simple_task' in results['started']
304
+ assert 'simple_task_1' in results['started']
305
+ assert 'simple_task_2' in results['started']
306
+
307
+
308
+ @pytest.mark.asyncio
309
+ async def test_empty_runner():
310
+ """Test that runner works with no tasks."""
311
+ runner = AsyncTaskRunner(verbose=False)
312
+
313
+ # Run with no tasks
314
+ results = await runner.get_aggregated_results()
315
+
316
+ # Should return empty but initialized state
317
+ assert results == {
318
+ 'results': {},
319
+ 'errors': {},
320
+ 'completed': [],
321
+ 'started': [],
322
+ 'retries': [],
323
+ 'timed_out': []
324
+ }
325
+
326
+
327
+ @pytest.mark.asyncio
328
+ async def test_task_config_none_values():
329
+ """Test that TaskConfig with None values falls back to global settings."""
330
+ runner = AsyncTaskRunner(
331
+ timeout=5,
332
+ retry_enabled=True,
333
+ verbose=False
334
+ )
335
+
336
+ # Add task with partial config (None values should use globals)
337
+ runner.add_task(
338
+ simple_task,
339
+ "test",
340
+ task_config=TaskConfig(
341
+ timeout=None, # Should use global (5)
342
+ retry_enabled=None, # Should use global (True)
343
+ metadata={'custom': 'data'}
344
+ )
345
+ )
346
+
347
+ results = await runner.get_aggregated_results()
348
+
349
+ # Task should complete successfully
350
+ assert 'simple_task' in results['results']
351
+ assert results['results']['simple_task'] == "Result: test"
352
+
353
+
354
+ @pytest.mark.asyncio
355
+ async def test_custom_task_names():
356
+ """Test custom task naming feature for better differentiation."""
357
+ runner = AsyncTaskRunner(verbose=False)
358
+
359
+ # Use custom task names to differentiate multiple calls to the same function
360
+ runner.add_task(simple_task, "API", task_name="fetch_api_data")
361
+ runner.add_task(simple_task, "Database", task_name="fetch_db_data")
362
+ runner.add_task(simple_task, "Cache", task_name="fetch_cache_data")
363
+
364
+ results = await runner.get_aggregated_results()
365
+
366
+ # Check that custom names were used
367
+ assert 'fetch_api_data' in results['results']
368
+ assert 'fetch_db_data' in results['results']
369
+ assert 'fetch_cache_data' in results['results']
370
+
371
+ # Check the results values
372
+ assert results['results']['fetch_api_data'] == "Result: API"
373
+ assert results['results']['fetch_db_data'] == "Result: Database"
374
+ assert results['results']['fetch_cache_data'] == "Result: Cache"
375
+
376
+ # Check completed list has custom names
377
+ assert set(results['completed']) == {'fetch_api_data', 'fetch_db_data', 'fetch_cache_data'}
378
+
379
+ # Check started list has custom names
380
+ assert set(results['started']) == {'fetch_api_data', 'fetch_db_data', 'fetch_cache_data'}
381
+
382
+
383
+ @pytest.mark.asyncio
384
+ async def test_custom_task_names_with_duplicates():
385
+ """Test that duplicate custom task names are automatically made unique."""
386
+ runner = AsyncTaskRunner(verbose=False)
387
+
388
+ # Add multiple tasks with the same custom name - should auto-suffix
389
+ runner.add_task(simple_task, "first", task_name="duplicate_name")
390
+ runner.add_task(simple_task, "second", task_name="duplicate_name")
391
+ runner.add_task(simple_task, "third", task_name="duplicate_name")
392
+
393
+ results = await runner.get_aggregated_results()
394
+
395
+ # Check that names were made unique with suffixes
396
+ assert 'duplicate_name' in results['results']
397
+ assert 'duplicate_name_1' in results['results']
398
+ assert 'duplicate_name_2' in results['results']
399
+
400
+ # Check the results values
401
+ assert results['results']['duplicate_name'] == "Result: first"
402
+ assert results['results']['duplicate_name_1'] == "Result: second"
403
+ assert results['results']['duplicate_name_2'] == "Result: third"
404
+
405
+ # Check completed list has unique names
406
+ assert 'duplicate_name' in results['completed']
407
+ assert 'duplicate_name_1' in results['completed']
408
+ assert 'duplicate_name_2' in results['completed']
409
+
410
+
411
+ if __name__ == "__main__":
412
+ # Run tests with asyncio
413
+ asyncio.run(test_default_callbacks_basic())
414
+ asyncio.run(test_default_callbacks_with_errors())
415
+ asyncio.run(test_default_callbacks_with_retry())
416
+ asyncio.run(test_custom_callback_override())
417
+ asyncio.run(test_no_default_callbacks())
418
+ asyncio.run(test_per_task_timeout_with_defaults())
419
+ asyncio.run(test_shared_state_persistence())
420
+ asyncio.run(test_verbose_mode())
421
+ asyncio.run(test_multiple_tasks_same_name())
422
+ asyncio.run(test_empty_runner())
423
+ asyncio.run(test_task_config_none_values())
424
+ asyncio.run(test_custom_task_names())
425
+ asyncio.run(test_custom_task_names_with_duplicates())
426
+ print("All tests passed!")