coco-framework 0.1.0__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 (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,941 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import concurrent.futures
5
+ import inspect
6
+ import threading
7
+ import time
8
+ import weakref
9
+ from collections.abc import (
10
+ AsyncIterator,
11
+ Awaitable,
12
+ Callable,
13
+ Collection,
14
+ Iterable,
15
+ Iterator,
16
+ Mapping,
17
+ Sequence,
18
+ )
19
+ from functools import partial
20
+ from typing import (
21
+ Any,
22
+ Generic,
23
+ TypeVar,
24
+ cast,
25
+ )
26
+
27
+ from coco_core.callbacks import Callbacks
28
+
29
+ from coco_graph._internal._constants import (
30
+ CONF,
31
+ CONFIG_KEY_CALL,
32
+ CONFIG_KEY_SCRATCHPAD,
33
+ ERROR,
34
+ ERROR_SOURCE_NODE,
35
+ INTERRUPT,
36
+ NO_WRITES,
37
+ RESUME,
38
+ RETURN,
39
+ )
40
+ from coco_graph._internal._future import chain_future, run_coroutine_threadsafe
41
+ from coco_graph._internal._scratchpad import PregelScratchpad
42
+ from coco_graph._internal._typing import MISSING
43
+ from coco_graph.constants import TAG_HIDDEN
44
+ from coco_graph.errors import GraphBubbleUp, GraphInterrupt
45
+ from coco_graph.pregel._algo import Call
46
+ from coco_graph.pregel._executor import Submit
47
+ from coco_graph.pregel._retry import arun_with_retry, run_with_retry
48
+ from coco_graph.types import (
49
+ CachePolicy,
50
+ PregelExecutableTask,
51
+ RetryPolicy,
52
+ TimeoutPolicy,
53
+ )
54
+
55
+ F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
56
+ E = TypeVar("E", threading.Event, asyncio.Event)
57
+
58
+ # List of filenames to exclude from exception traceback
59
+ # Note: Frames will be removed if they are the last frame in traceback, recursively
60
+ EXCLUDED_FRAME_FNAMES = (
61
+ "coco_graph/pregel/retry.py",
62
+ "coco_graph/pregel/runner.py",
63
+ "coco_graph/pregel/executor.py",
64
+ "coco_graph/utils/runnable.py",
65
+ "coco_core/runnables/config.py",
66
+ "concurrent/futures/thread.py",
67
+ "concurrent/futures/_base.py",
68
+ )
69
+
70
+ SKIP_RERAISE_SET: weakref.WeakSet[concurrent.futures.Future | asyncio.Future] = (
71
+ weakref.WeakSet()
72
+ )
73
+
74
+
75
+ class FuturesDict(Generic[F, E], dict[F, PregelExecutableTask | None]):
76
+ event: E
77
+ callback: weakref.ref[Callable[[PregelExecutableTask, BaseException | None], None]]
78
+ # Stop condition is injected by PregelRunner instead of hard-coded here.
79
+ # This lets the runner treat graph-error-handled exceptions as non-fatal
80
+ # so `on_done` does not trigger an early stop for those futures.
81
+ should_stop: Callable[[set[F]], bool]
82
+ counter: int
83
+ done: set[F]
84
+ lock: threading.Lock
85
+
86
+ def __init__(
87
+ self,
88
+ event: E,
89
+ callback: weakref.ref[
90
+ Callable[[PregelExecutableTask, BaseException | None], None]
91
+ ],
92
+ should_stop: Callable[[set[F]], bool],
93
+ future_type: type[F],
94
+ # used for generic typing, newer py supports FutureDict[...](...)
95
+ ) -> None:
96
+ super().__init__()
97
+ self.lock = threading.Lock()
98
+ self.event = event
99
+ self.callback = callback
100
+ self.should_stop = should_stop
101
+ self.counter = 0
102
+ self.done: set[F] = set()
103
+
104
+ def __setitem__(
105
+ self,
106
+ key: F,
107
+ value: PregelExecutableTask | None,
108
+ ) -> None:
109
+ super().__setitem__(key, value) # type: ignore[index]
110
+ if value is not None:
111
+ with self.lock:
112
+ self.event.clear()
113
+ self.counter += 1
114
+ key.add_done_callback(partial(self.on_done, value))
115
+
116
+ def on_done(
117
+ self,
118
+ task: PregelExecutableTask,
119
+ fut: F,
120
+ ) -> None:
121
+ # Called automatically by future.add_done_callback registered in __setitem__.
122
+ try:
123
+ if cb := self.callback():
124
+ cb(task, _exception(fut))
125
+ finally:
126
+ with self.lock:
127
+ self.done.add(fut)
128
+ self.counter -= 1
129
+ # Wake waiter when all tracked futures are done, or when runner-level
130
+ # stop condition is met (for example, a non-handled fatal exception).
131
+ if self.counter == 0 or self.should_stop(self.done):
132
+ self.event.set()
133
+
134
+
135
+ class PregelRunner:
136
+ """Responsible for executing a set of Pregel tasks concurrently, committing
137
+ their writes, yielding control to caller when there is output to emit, and
138
+ interrupting other tasks if appropriate."""
139
+
140
+ def __init__(
141
+ self,
142
+ *,
143
+ submit: weakref.ref[Submit],
144
+ put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],
145
+ use_astream: bool = False,
146
+ node_finished: Callable[[str], None] | None = None,
147
+ node_error_handler_map: Mapping[str, str] | None = None,
148
+ schedule_error_handler: Callable[
149
+ [PregelExecutableTask, BaseException], PregelExecutableTask | None
150
+ ]
151
+ | None = None,
152
+ aschedule_error_handler: Callable[
153
+ [PregelExecutableTask, BaseException],
154
+ Awaitable[PregelExecutableTask | None],
155
+ ]
156
+ | None = None,
157
+ ) -> None:
158
+ self.submit = submit
159
+ self.put_writes = put_writes
160
+ self.use_astream = use_astream
161
+ self.node_finished = node_finished
162
+ self.node_error_handler_map = dict(node_error_handler_map or {})
163
+ self.error_handler_nodes = set(self.node_error_handler_map.values())
164
+ self.schedule_error_handler = schedule_error_handler
165
+ self.aschedule_error_handler = aschedule_error_handler
166
+ # Exception object ids that are already routed to graph-level error handler.
167
+ # These ids are consulted by stop/panic checks to avoid re-raising handled
168
+ # exceptions via the normal fatal path in the same run.
169
+ self._handled_exception_ids: set[int] = set()
170
+
171
+ def _should_route_to_error_handler(self, task: PregelExecutableTask) -> bool:
172
+ if task.name in self.error_handler_nodes:
173
+ return False
174
+ return task.name in self.node_error_handler_map
175
+
176
+ def tick(
177
+ self,
178
+ tasks: Iterable[PregelExecutableTask],
179
+ *,
180
+ reraise: bool = True,
181
+ timeout: float | None = None,
182
+ retry_policy: Sequence[RetryPolicy] | None = None,
183
+ get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None,
184
+ schedule_task: Callable[
185
+ [PregelExecutableTask, int, Call | None],
186
+ PregelExecutableTask | None,
187
+ ],
188
+ ) -> Iterator[None]:
189
+ tasks = tuple(tasks)
190
+ futures = FuturesDict(
191
+ callback=weakref.WeakMethod(self.commit),
192
+ event=threading.Event(),
193
+ should_stop=partial(
194
+ _should_stop_others, handled_exception_ids=self._handled_exception_ids
195
+ ),
196
+ future_type=concurrent.futures.Future,
197
+ )
198
+ # give control back to the caller
199
+ yield
200
+ # fast path if single task with no timeout and no waiter
201
+ if len(tasks) == 0:
202
+ return
203
+ elif len(tasks) == 1 and timeout is None and get_waiter is None:
204
+ t = tasks[0]
205
+ scheduled_error_handler = False
206
+ try:
207
+ run_with_retry(
208
+ t,
209
+ retry_policy,
210
+ configurable={
211
+ CONFIG_KEY_CALL: partial(
212
+ _call,
213
+ weakref.ref(t),
214
+ retry_policy=retry_policy,
215
+ futures=weakref.ref(futures),
216
+ schedule_task=schedule_task,
217
+ submit=self.submit,
218
+ ),
219
+ },
220
+ )
221
+ self.commit(t, None)
222
+ except Exception as exc:
223
+ self.commit(t, exc)
224
+ if (
225
+ not isinstance(exc, GraphBubbleUp)
226
+ and self._should_route_to_error_handler(t)
227
+ and self.schedule_error_handler is not None
228
+ ):
229
+ self._handled_exception_ids.add(id(exc))
230
+ if handler_task := self.schedule_error_handler(t, exc):
231
+ tasks = (handler_task,)
232
+ scheduled_error_handler = True
233
+ # Continue to the regular scheduling path for handler execution.
234
+ if reraise and futures:
235
+ if id(exc) not in self._handled_exception_ids:
236
+ # will be re-raised after futures are done
237
+ fut: concurrent.futures.Future = concurrent.futures.Future()
238
+ fut.set_exception(exc)
239
+ futures.done.add(fut)
240
+ elif reraise and id(exc) not in self._handled_exception_ids:
241
+ if tb := exc.__traceback__:
242
+ while tb.tb_next is not None and any(
243
+ tb.tb_frame.f_code.co_filename.endswith(name)
244
+ for name in EXCLUDED_FRAME_FNAMES
245
+ ):
246
+ tb = tb.tb_next
247
+ exc.__traceback__ = tb
248
+ raise
249
+ if not futures and not scheduled_error_handler:
250
+ # maybe `t` scheduled another task
251
+ return
252
+ else:
253
+ if not scheduled_error_handler:
254
+ tasks = () # don't reschedule this task
255
+ # add waiter task if requested
256
+ if get_waiter is not None:
257
+ futures[get_waiter()] = None
258
+ # schedule tasks
259
+ for t in tasks:
260
+ fut = self.submit()( # type: ignore[misc]
261
+ run_with_retry,
262
+ t,
263
+ retry_policy,
264
+ configurable={
265
+ CONFIG_KEY_CALL: partial(
266
+ _call,
267
+ weakref.ref(t),
268
+ retry_policy=retry_policy,
269
+ futures=weakref.ref(futures),
270
+ schedule_task=schedule_task,
271
+ submit=self.submit,
272
+ ),
273
+ },
274
+ __reraise_on_exit__=reraise,
275
+ )
276
+ futures[fut] = t
277
+ # execute tasks, and wait for one to fail or all to finish.
278
+ # each task is independent from all other concurrent tasks
279
+ # yield updates/debug output as each task finishes
280
+ end_time = timeout + time.monotonic() if timeout else None
281
+ handled_futures: set[concurrent.futures.Future[Any]] = set()
282
+ while len(futures) > (1 if get_waiter is not None else 0):
283
+ done, inflight = concurrent.futures.wait(
284
+ futures,
285
+ return_when=concurrent.futures.FIRST_COMPLETED,
286
+ timeout=(max(0, end_time - time.monotonic()) if end_time else None),
287
+ )
288
+ if not done:
289
+ break # timed out
290
+ done_for_stop: set[concurrent.futures.Future[Any]] = set()
291
+ for fut in done:
292
+ task = futures.pop(fut)
293
+ if task is None:
294
+ # waiter task finished, schedule another
295
+ if inflight and get_waiter is not None:
296
+ futures[get_waiter()] = None
297
+ elif (
298
+ (task_exc := _exception(fut))
299
+ and self._should_route_to_error_handler(task)
300
+ and not isinstance(task_exc, GraphBubbleUp)
301
+ ):
302
+ self._handled_exception_ids.add(id(task_exc))
303
+ SKIP_RERAISE_SET.add(fut)
304
+ handled_futures.add(fut)
305
+ if self.schedule_error_handler is not None:
306
+ if handler_task := self.schedule_error_handler(task, task_exc):
307
+ handler_fut = self.submit()( # type: ignore[misc]
308
+ run_with_retry,
309
+ handler_task,
310
+ retry_policy,
311
+ configurable={
312
+ CONFIG_KEY_CALL: partial(
313
+ _call,
314
+ weakref.ref(handler_task),
315
+ retry_policy=retry_policy,
316
+ futures=weakref.ref(futures),
317
+ schedule_task=schedule_task,
318
+ submit=self.submit,
319
+ ),
320
+ },
321
+ __reraise_on_exit__=reraise,
322
+ )
323
+ futures[handler_fut] = handler_task
324
+ else:
325
+ done_for_stop.add(fut)
326
+ else:
327
+ # remove references to loop vars
328
+ del fut, task
329
+ # maybe stop other tasks
330
+ if _should_stop_others(
331
+ done_for_stop, handled_exception_ids=self._handled_exception_ids
332
+ ):
333
+ break
334
+ # give control back to the caller
335
+ yield
336
+ # wait for done callbacks
337
+ futures.event.wait(
338
+ timeout=(max(0, end_time - time.monotonic()) if end_time else None)
339
+ )
340
+ # give control back to the caller
341
+ yield
342
+ # panic on failure or timeout
343
+ try:
344
+ _panic_or_proceed(
345
+ futures.done.union(f for f, t in futures.items() if t is not None),
346
+ panic=reraise,
347
+ handled_exception_ids=self._handled_exception_ids,
348
+ handled_futures=handled_futures,
349
+ )
350
+ except Exception as exc:
351
+ if tb := exc.__traceback__:
352
+ while tb.tb_next is not None and any(
353
+ tb.tb_frame.f_code.co_filename.endswith(name)
354
+ for name in EXCLUDED_FRAME_FNAMES
355
+ ):
356
+ tb = tb.tb_next
357
+ exc.__traceback__ = tb
358
+ raise
359
+
360
+ async def atick(
361
+ self,
362
+ tasks: Iterable[PregelExecutableTask],
363
+ *,
364
+ reraise: bool = True,
365
+ timeout: float | None = None,
366
+ retry_policy: Sequence[RetryPolicy] | None = None,
367
+ get_waiter: Callable[[], asyncio.Future[None]] | None = None,
368
+ schedule_task: Callable[
369
+ [PregelExecutableTask, int, Call | None],
370
+ Awaitable[PregelExecutableTask | None],
371
+ ],
372
+ ) -> AsyncIterator[None]:
373
+ try:
374
+ loop = asyncio.get_event_loop()
375
+ except RuntimeError:
376
+ loop = asyncio.new_event_loop()
377
+ asyncio.set_event_loop(loop)
378
+ tasks = tuple(tasks)
379
+ futures = FuturesDict(
380
+ callback=weakref.WeakMethod(self.commit),
381
+ event=asyncio.Event(),
382
+ should_stop=partial(
383
+ _should_stop_others, handled_exception_ids=self._handled_exception_ids
384
+ ),
385
+ future_type=asyncio.Future,
386
+ )
387
+ # give control back to the caller
388
+ yield
389
+ # fast path if single task with no waiter and no timeout
390
+ if len(tasks) == 0:
391
+ return
392
+ elif len(tasks) == 1 and get_waiter is None and timeout is None:
393
+ t = tasks[0]
394
+ scheduled_error_handler = False
395
+ try:
396
+ await arun_with_retry(
397
+ t,
398
+ retry_policy,
399
+ stream=self.use_astream,
400
+ configurable={
401
+ CONFIG_KEY_CALL: partial(
402
+ _acall,
403
+ weakref.ref(t),
404
+ stream=self.use_astream,
405
+ retry_policy=retry_policy,
406
+ futures=weakref.ref(futures),
407
+ schedule_task=schedule_task,
408
+ submit=self.submit,
409
+ loop=loop,
410
+ ),
411
+ },
412
+ )
413
+ self.commit(t, None)
414
+ except Exception as exc:
415
+ self.commit(t, exc)
416
+ if (
417
+ not isinstance(exc, GraphBubbleUp)
418
+ and self._should_route_to_error_handler(t)
419
+ and self.aschedule_error_handler is not None
420
+ ):
421
+ self._handled_exception_ids.add(id(exc))
422
+ if handler_task := await self.aschedule_error_handler(t, exc):
423
+ tasks = (handler_task,)
424
+ scheduled_error_handler = True
425
+ if reraise and futures:
426
+ if id(exc) not in self._handled_exception_ids:
427
+ # will be re-raised after futures are done
428
+ fut: asyncio.Future = loop.create_future()
429
+ fut.set_exception(exc)
430
+ futures.done.add(fut)
431
+ elif reraise and id(exc) not in self._handled_exception_ids:
432
+ if tb := exc.__traceback__:
433
+ while tb.tb_next is not None and any(
434
+ tb.tb_frame.f_code.co_filename.endswith(name)
435
+ for name in EXCLUDED_FRAME_FNAMES
436
+ ):
437
+ tb = tb.tb_next
438
+ exc.__traceback__ = tb
439
+ raise
440
+ if not futures and not scheduled_error_handler:
441
+ # maybe `t` scheduled another task
442
+ return
443
+ else:
444
+ if not scheduled_error_handler:
445
+ tasks = () # don't reschedule this task
446
+ # add waiter task if requested
447
+ if get_waiter is not None:
448
+ futures[get_waiter()] = None
449
+ # schedule tasks
450
+ for t in tasks:
451
+ fut = cast(
452
+ asyncio.Future,
453
+ self.submit()( # type: ignore[misc]
454
+ arun_with_retry,
455
+ t,
456
+ retry_policy,
457
+ stream=self.use_astream,
458
+ configurable={
459
+ CONFIG_KEY_CALL: partial(
460
+ _acall,
461
+ weakref.ref(t),
462
+ retry_policy=retry_policy,
463
+ stream=self.use_astream,
464
+ futures=weakref.ref(futures),
465
+ schedule_task=schedule_task,
466
+ submit=self.submit,
467
+ loop=loop,
468
+ ),
469
+ },
470
+ __name__=t.name,
471
+ __cancel_on_exit__=True,
472
+ __reraise_on_exit__=reraise,
473
+ ),
474
+ )
475
+ futures[fut] = t
476
+ # execute tasks, and wait for one to fail or all to finish.
477
+ # each task is independent from all other concurrent tasks
478
+ # yield updates/debug output as each task finishes
479
+ end_time = timeout + loop.time() if timeout else None
480
+ handled_futures: set[asyncio.Future[Any]] = set()
481
+ while len(futures) > (1 if get_waiter is not None else 0):
482
+ done, inflight = await asyncio.wait(
483
+ futures,
484
+ return_when=asyncio.FIRST_COMPLETED,
485
+ timeout=(max(0, end_time - loop.time()) if end_time else None),
486
+ )
487
+ if not done:
488
+ break # timed out
489
+ done_for_stop: set[asyncio.Future[Any]] = set()
490
+ for fut in done:
491
+ task = futures.pop(fut)
492
+ if task is None:
493
+ # waiter task finished, schedule another
494
+ if inflight and get_waiter is not None:
495
+ futures[get_waiter()] = None
496
+ elif (
497
+ (task_exc := _exception(fut))
498
+ and self._should_route_to_error_handler(task)
499
+ and not isinstance(task_exc, GraphBubbleUp)
500
+ ):
501
+ self._handled_exception_ids.add(id(task_exc))
502
+ SKIP_RERAISE_SET.add(fut)
503
+ handled_futures.add(fut)
504
+ if self.aschedule_error_handler is not None:
505
+ if handler_task := await self.aschedule_error_handler(
506
+ task, task_exc
507
+ ):
508
+ handler_fut = cast(
509
+ asyncio.Future,
510
+ self.submit()( # type: ignore[misc]
511
+ arun_with_retry,
512
+ handler_task,
513
+ retry_policy,
514
+ stream=self.use_astream,
515
+ configurable={
516
+ CONFIG_KEY_CALL: partial(
517
+ _acall,
518
+ weakref.ref(handler_task),
519
+ retry_policy=retry_policy,
520
+ stream=self.use_astream,
521
+ futures=weakref.ref(futures),
522
+ schedule_task=schedule_task,
523
+ submit=self.submit,
524
+ loop=loop,
525
+ ),
526
+ },
527
+ __name__=handler_task.name,
528
+ __cancel_on_exit__=True,
529
+ __reraise_on_exit__=reraise,
530
+ ),
531
+ )
532
+ futures[handler_fut] = handler_task
533
+ else:
534
+ done_for_stop.add(fut)
535
+ else:
536
+ # remove references to loop vars
537
+ del fut, task
538
+ # maybe stop other tasks
539
+ if _should_stop_others(
540
+ done_for_stop, handled_exception_ids=self._handled_exception_ids
541
+ ):
542
+ break
543
+ # give control back to the caller
544
+ yield
545
+ # wait for done callbacks
546
+ await asyncio.wait_for(
547
+ futures.event.wait(),
548
+ timeout=(max(0, end_time - loop.time()) if end_time else None),
549
+ )
550
+ # give control back to the caller
551
+ yield
552
+ # cancel waiter task
553
+ for fut in futures:
554
+ fut.cancel()
555
+ # panic on failure or timeout
556
+ try:
557
+ _panic_or_proceed(
558
+ futures.done.union(f for f, t in futures.items() if t is not None),
559
+ timeout_exc_cls=asyncio.TimeoutError,
560
+ panic=reraise,
561
+ handled_exception_ids=self._handled_exception_ids,
562
+ handled_futures=handled_futures,
563
+ )
564
+ except Exception as exc:
565
+ if tb := exc.__traceback__:
566
+ while tb.tb_next is not None and any(
567
+ tb.tb_frame.f_code.co_filename.endswith(name)
568
+ for name in EXCLUDED_FRAME_FNAMES
569
+ ):
570
+ tb = tb.tb_next
571
+ exc.__traceback__ = tb
572
+ raise
573
+
574
+ def commit(
575
+ self,
576
+ task: PregelExecutableTask,
577
+ exception: BaseException | None,
578
+ ) -> None:
579
+ if isinstance(exception, asyncio.CancelledError):
580
+ # for cancelled tasks, also save error in task,
581
+ # so loop can finish super-step
582
+ task.writes.append((ERROR, exception))
583
+ self.put_writes()(task.id, task.writes) # type: ignore[misc]
584
+ elif exception:
585
+ if isinstance(exception, GraphInterrupt):
586
+ # save interrupt to checkpointer
587
+ if exception.args[0]:
588
+ writes = [(INTERRUPT, exception.args[0])]
589
+ if resumes := [w for w in task.writes if w[0] == RESUME]:
590
+ writes.extend(resumes)
591
+ self.put_writes()(task.id, writes) # type: ignore[misc]
592
+ elif isinstance(exception, GraphBubbleUp):
593
+ # exception will be raised in _panic_or_proceed
594
+ pass
595
+ else:
596
+ # save error to checkpointer
597
+ task.writes.append((ERROR, exception))
598
+ if self._should_route_to_error_handler(task) and not isinstance(
599
+ exception, GraphBubbleUp
600
+ ):
601
+ task.writes.append((ERROR_SOURCE_NODE, task.name))
602
+ self._handled_exception_ids.add(id(exception))
603
+ self.put_writes()(task.id, task.writes) # type: ignore[misc]
604
+ else:
605
+ if self.node_finished and (
606
+ task.config is None or TAG_HIDDEN not in task.config.get("tags", [])
607
+ ):
608
+ self.node_finished(task.name)
609
+ if not task.writes:
610
+ # add no writes marker
611
+ task.writes.append((NO_WRITES, None))
612
+ # save task writes to checkpointer
613
+ self.put_writes()(task.id, task.writes) # type: ignore[misc]
614
+
615
+
616
+ def _should_stop_others(
617
+ done: set[F],
618
+ *,
619
+ handled_exception_ids: set[int] | None = None,
620
+ ) -> bool:
621
+ """Check if any task failed, if so, cancel all other tasks.
622
+ GraphInterrupts are not considered failures."""
623
+ for fut in done:
624
+ if fut.cancelled():
625
+ continue
626
+ elif exc := fut.exception():
627
+ if (
628
+ id(exc) not in (handled_exception_ids or set())
629
+ and not isinstance(exc, GraphBubbleUp)
630
+ and fut not in SKIP_RERAISE_SET
631
+ ):
632
+ return True
633
+
634
+ return False
635
+
636
+
637
+ def _exception(
638
+ fut: concurrent.futures.Future[Any] | asyncio.Future[Any],
639
+ ) -> BaseException | None:
640
+ """Return the exception from a future, without raising CancelledError."""
641
+ if fut.cancelled():
642
+ if isinstance(fut, asyncio.Future):
643
+ return asyncio.CancelledError()
644
+ else:
645
+ return concurrent.futures.CancelledError()
646
+ else:
647
+ return fut.exception()
648
+
649
+
650
+ def _panic_or_proceed(
651
+ futs: set[concurrent.futures.Future] | set[asyncio.Future],
652
+ *,
653
+ timeout_exc_cls: type[Exception] = TimeoutError,
654
+ panic: bool = True,
655
+ handled_exception_ids: set[int] | None = None,
656
+ handled_futures: Collection[concurrent.futures.Future[Any] | asyncio.Future[Any]]
657
+ | None = None,
658
+ ) -> None:
659
+ """Cancel remaining tasks if any failed, re-raise exception if panic is True."""
660
+ done: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
661
+ inflight: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
662
+ for fut in futs:
663
+ if fut.cancelled():
664
+ continue
665
+ elif fut.done():
666
+ done.add(fut)
667
+ else:
668
+ inflight.add(fut)
669
+ interrupts: list[GraphInterrupt] = []
670
+ while done:
671
+ # if any task failed
672
+ fut = done.pop()
673
+ if exc := _exception(fut):
674
+ if fut in (handled_futures or set()):
675
+ continue
676
+ if id(exc) in (handled_exception_ids or set()):
677
+ continue
678
+ # cancel all pending tasks
679
+ while inflight:
680
+ inflight.pop().cancel()
681
+ # raise the exception
682
+ if panic:
683
+ if isinstance(exc, GraphInterrupt):
684
+ # collect interrupts
685
+ interrupts.append(exc)
686
+ elif fut not in SKIP_RERAISE_SET:
687
+ raise exc
688
+ # raise combined interrupts
689
+ if interrupts:
690
+ raise GraphInterrupt(tuple(i for exc in interrupts for i in exc.args[0]))
691
+ if inflight:
692
+ # if we got here means we timed out
693
+ while inflight:
694
+ # cancel all pending tasks
695
+ inflight.pop().cancel()
696
+ # raise timeout error
697
+ raise timeout_exc_cls("Timed out")
698
+
699
+
700
+ def _call(
701
+ task: weakref.ref[PregelExecutableTask],
702
+ func: Callable[[Any], Awaitable[Any] | Any],
703
+ input: Any,
704
+ *,
705
+ retry_policy: Sequence[RetryPolicy] | None = None,
706
+ cache_policy: CachePolicy | None = None,
707
+ timeout: TimeoutPolicy | None = None,
708
+ callbacks: Callbacks = None,
709
+ futures: weakref.ref[FuturesDict],
710
+ schedule_task: Callable[
711
+ [PregelExecutableTask, int, Call | None], PregelExecutableTask | None
712
+ ],
713
+ submit: weakref.ref[Submit],
714
+ ) -> concurrent.futures.Future[Any]:
715
+ if inspect.iscoroutinefunction(func):
716
+ raise RuntimeError("In an sync context async tasks cannot be called")
717
+
718
+ fut: concurrent.futures.Future | None = None
719
+ # schedule PUSH tasks, collect futures
720
+ scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
721
+ # schedule the next task, if the callback returns one
722
+ if next_task := schedule_task(
723
+ task(), # type: ignore[arg-type]
724
+ scratchpad.call_counter(),
725
+ Call(
726
+ func,
727
+ input,
728
+ retry_policy=retry_policy,
729
+ cache_policy=cache_policy,
730
+ callbacks=callbacks,
731
+ timeout=timeout,
732
+ ),
733
+ ):
734
+ if fut := next(
735
+ (
736
+ f
737
+ for f, t in list(futures().items()) # type: ignore[union-attr]
738
+ if t is not None and t == next_task.id
739
+ ),
740
+ None,
741
+ ):
742
+ # if the parent task was retried,
743
+ # the next task might already be running
744
+ pass
745
+ elif next_task.writes:
746
+ # if it already ran, return the result
747
+ fut = concurrent.futures.Future()
748
+ ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)
749
+ if ret is not MISSING:
750
+ fut.set_result(ret)
751
+ elif exc := next((v for c, v in next_task.writes if c == ERROR), None):
752
+ fut.set_exception(
753
+ exc if isinstance(exc, BaseException) else Exception(exc)
754
+ )
755
+ else:
756
+ fut.set_result(None)
757
+ else:
758
+ # schedule the next task
759
+ fut = submit()( # type: ignore[misc]
760
+ run_with_retry,
761
+ next_task,
762
+ retry_policy,
763
+ configurable={
764
+ CONFIG_KEY_CALL: partial(
765
+ _call,
766
+ weakref.ref(next_task),
767
+ futures=futures,
768
+ retry_policy=retry_policy,
769
+ callbacks=callbacks,
770
+ schedule_task=schedule_task,
771
+ submit=submit,
772
+ ),
773
+ },
774
+ __reraise_on_exit__=False,
775
+ # starting a new task in the next tick ensures
776
+ # updates from this tick are committed/streamed first
777
+ __next_tick__=True,
778
+ )
779
+ # exceptions for call() tasks are raised into the parent task
780
+ # so we should not re-raise at the end of the tick
781
+ SKIP_RERAISE_SET.add(fut)
782
+ futures()[fut] = next_task # type: ignore[index]
783
+ fut = cast(asyncio.Future | concurrent.futures.Future, fut)
784
+ # return a chained future to ensure commit() callback is called
785
+ # before the returned future is resolved, to ensure stream order etc
786
+ return chain_future(fut, concurrent.futures.Future())
787
+
788
+
789
+ def _acall(
790
+ task: weakref.ref[PregelExecutableTask],
791
+ func: Callable[[Any], Awaitable[Any] | Any],
792
+ input: Any,
793
+ *,
794
+ retry_policy: Sequence[RetryPolicy] | None = None,
795
+ cache_policy: CachePolicy | None = None,
796
+ timeout: TimeoutPolicy | None = None,
797
+ callbacks: Callbacks = None,
798
+ # injected dependencies
799
+ futures: weakref.ref[FuturesDict],
800
+ schedule_task: Callable[
801
+ [PregelExecutableTask, int, Call | None],
802
+ Awaitable[PregelExecutableTask | None],
803
+ ],
804
+ submit: weakref.ref[Submit],
805
+ loop: asyncio.AbstractEventLoop,
806
+ stream: bool = False,
807
+ ) -> asyncio.Future[Any] | concurrent.futures.Future[Any]:
808
+ # return a chained future to ensure commit() callback is called
809
+ # before the returned future is resolved, to ensure stream order etc
810
+ try:
811
+ in_async = asyncio.current_task() is not None
812
+ except RuntimeError:
813
+ in_async = False
814
+ # if in async context return an async future, otherwise return a sync future
815
+ if in_async:
816
+ fut: asyncio.Future[Any] | concurrent.futures.Future[Any] = asyncio.Future(
817
+ loop=loop
818
+ )
819
+ else:
820
+ fut = concurrent.futures.Future()
821
+ # schedule the next task
822
+ run_coroutine_threadsafe(
823
+ _acall_impl(
824
+ fut,
825
+ task,
826
+ func,
827
+ input,
828
+ retry_policy=retry_policy,
829
+ cache_policy=cache_policy,
830
+ timeout=timeout,
831
+ callbacks=callbacks,
832
+ futures=futures,
833
+ schedule_task=schedule_task,
834
+ submit=submit,
835
+ loop=loop,
836
+ stream=stream,
837
+ ),
838
+ loop,
839
+ lazy=False,
840
+ )
841
+ return fut
842
+
843
+
844
+ async def _acall_impl(
845
+ destination: asyncio.Future[Any] | concurrent.futures.Future[Any],
846
+ task: weakref.ref[PregelExecutableTask],
847
+ func: Callable[[Any], Awaitable[Any] | Any],
848
+ input: Any,
849
+ *,
850
+ retry_policy: Sequence[RetryPolicy] | None = None,
851
+ cache_policy: CachePolicy | None = None,
852
+ timeout: TimeoutPolicy | None = None,
853
+ callbacks: Callbacks = None,
854
+ # injected dependencies
855
+ futures: weakref.ref[FuturesDict[asyncio.Future, asyncio.Event]],
856
+ schedule_task: Callable[
857
+ [PregelExecutableTask, int, Call | None],
858
+ Awaitable[PregelExecutableTask | None],
859
+ ],
860
+ submit: weakref.ref[Submit],
861
+ loop: asyncio.AbstractEventLoop,
862
+ stream: bool = False,
863
+ ) -> None:
864
+ try:
865
+ fut: asyncio.Future | None = None
866
+ # schedule PUSH tasks, collect futures
867
+ scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
868
+ # schedule the next task, if the callback returns one
869
+ if next_task := await schedule_task(
870
+ task(), # type: ignore[arg-type]
871
+ scratchpad.call_counter(),
872
+ Call(
873
+ func,
874
+ input,
875
+ retry_policy=retry_policy,
876
+ cache_policy=cache_policy,
877
+ callbacks=callbacks,
878
+ timeout=timeout,
879
+ ),
880
+ ):
881
+ if fut := next(
882
+ (
883
+ f
884
+ for f, t in list(futures().items()) # type: ignore[union-attr]
885
+ if t is not None and t == next_task.id
886
+ ),
887
+ None,
888
+ ):
889
+ # if the parent task was retried,
890
+ # the next task might already be running
891
+ pass
892
+ elif next_task.writes:
893
+ # if it already ran, return the result
894
+ fut = asyncio.Future(loop=loop)
895
+ ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)
896
+ if ret is not MISSING:
897
+ fut.set_result(ret)
898
+ elif exc := next((v for c, v in next_task.writes if c == ERROR), None):
899
+ fut.set_exception(
900
+ exc if isinstance(exc, BaseException) else Exception(exc)
901
+ )
902
+ else:
903
+ fut.set_result(None)
904
+ else:
905
+ # schedule the next task
906
+ fut = cast(
907
+ asyncio.Future,
908
+ submit()( # type: ignore[misc]
909
+ arun_with_retry,
910
+ next_task,
911
+ retry_policy,
912
+ stream=stream,
913
+ configurable={
914
+ CONFIG_KEY_CALL: partial(
915
+ _acall,
916
+ weakref.ref(next_task),
917
+ stream=stream,
918
+ futures=futures,
919
+ schedule_task=schedule_task,
920
+ submit=submit,
921
+ loop=loop,
922
+ ),
923
+ },
924
+ __name__=next_task.name,
925
+ __cancel_on_exit__=True,
926
+ __reraise_on_exit__=False,
927
+ # starting a new task in the next tick ensures
928
+ # updates from this tick are committed/streamed first
929
+ __next_tick__=True,
930
+ ),
931
+ )
932
+ # exceptions for call() tasks are raised into the parent task
933
+ # so we should not re-raise at the end of the tick
934
+ SKIP_RERAISE_SET.add(fut)
935
+ futures()[fut] = next_task # type: ignore[index]
936
+ if fut is not None:
937
+ chain_future(fut, destination)
938
+ else:
939
+ destination.set_exception(RuntimeError("Task not scheduled"))
940
+ except Exception as exc:
941
+ destination.set_exception(exc)