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,602 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from collections import defaultdict
5
+ from collections.abc import Iterator, Mapping, Sequence
6
+ from contextlib import contextmanager
7
+ from typing import Any, cast
8
+
9
+ from coco_core.runnables import RunnableConfig
10
+ from coco_graph.checkpoint.base import (
11
+ WRITES_IDX_MAP,
12
+ ChannelVersions,
13
+ Checkpoint,
14
+ CheckpointMetadata,
15
+ CheckpointTuple,
16
+ DeltaChannelHistory,
17
+ get_checkpoint_id,
18
+ get_serializable_checkpoint_metadata,
19
+ )
20
+ from coco_graph.checkpoint.serde.base import SerializerProtocol
21
+ from coco_graph.checkpoint.serde.types import _DeltaSnapshot
22
+ from psycopg import Capabilities, Connection, Cursor, Pipeline
23
+ from psycopg.rows import DictRow, dict_row
24
+ from psycopg.types.json import Jsonb
25
+ from psycopg_pool import ConnectionPool
26
+
27
+ from coco_graph.checkpoint.postgres import _internal
28
+ from coco_graph.checkpoint.postgres.base import (
29
+ _DELTA_PAGE_SIZE,
30
+ BasePostgresSaver,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
33
+ _DeltaStage2Row,
34
+ )
35
+ from coco_graph.checkpoint.postgres.shallow import ShallowPostgresSaver
36
+
37
+ Conn = _internal.Conn # For backward compatibility
38
+
39
+
40
+ class PostgresSaver(BasePostgresSaver):
41
+ """Checkpointer that stores checkpoints in a Postgres database."""
42
+
43
+ lock: threading.Lock
44
+
45
+ def __init__(
46
+ self,
47
+ conn: _internal.Conn,
48
+ pipe: Pipeline | None = None,
49
+ serde: SerializerProtocol | None = None,
50
+ ) -> None:
51
+ super().__init__(serde=serde)
52
+ if isinstance(conn, ConnectionPool) and pipe is not None:
53
+ raise ValueError(
54
+ "Pipeline should be used only with a single Connection, not ConnectionPool."
55
+ )
56
+
57
+ self.conn = conn
58
+ self.pipe = pipe
59
+ self.lock = threading.Lock()
60
+ self.supports_pipeline = Capabilities().has_pipeline()
61
+
62
+ @classmethod
63
+ @contextmanager
64
+ def from_conn_string(
65
+ cls, conn_string: str, *, pipeline: bool = False
66
+ ) -> Iterator[PostgresSaver]:
67
+ """Create a new PostgresSaver instance from a connection string.
68
+
69
+ Args:
70
+ conn_string: The Postgres connection info string.
71
+ pipeline: whether to use Pipeline
72
+
73
+ Returns:
74
+ PostgresSaver: A new PostgresSaver instance.
75
+ """
76
+ with Connection.connect(
77
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
78
+ ) as conn:
79
+ if pipeline:
80
+ with conn.pipeline() as pipe:
81
+ yield cls(conn, pipe)
82
+ else:
83
+ yield cls(conn)
84
+
85
+ def setup(self) -> None:
86
+ """Set up the checkpoint database asynchronously.
87
+
88
+ This method creates the necessary tables in the Postgres database if they don't
89
+ already exist and runs database migrations. It MUST be called directly by the user
90
+ the first time checkpointer is used.
91
+ """
92
+ with self._cursor() as cur:
93
+ cur.execute(self.MIGRATIONS[0])
94
+ results = cur.execute(
95
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
96
+ )
97
+ row = results.fetchone()
98
+ if row is None:
99
+ version = -1
100
+ else:
101
+ version = row["v"]
102
+ for v, migration in zip(
103
+ range(version + 1, len(self.MIGRATIONS)),
104
+ self.MIGRATIONS[version + 1 :],
105
+ strict=False,
106
+ ):
107
+ cur.execute(migration)
108
+ cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
109
+ if self.pipe:
110
+ self.pipe.sync()
111
+
112
+ def list(
113
+ self,
114
+ config: RunnableConfig | None,
115
+ *,
116
+ filter: dict[str, Any] | None = None,
117
+ before: RunnableConfig | None = None,
118
+ limit: int | None = None,
119
+ ) -> Iterator[CheckpointTuple]:
120
+ """List checkpoints from the database.
121
+
122
+ This method retrieves a list of checkpoint tuples from the Postgres database based
123
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
124
+
125
+ Args:
126
+ config: The config to use for listing the checkpoints.
127
+ filter: Additional filtering criteria for metadata.
128
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
129
+ limit: The maximum number of checkpoints to return.
130
+
131
+ Yields:
132
+ An iterator of checkpoint tuples.
133
+
134
+ Examples:
135
+ >>> from coco_graph.checkpoint.postgres import PostgresSaver
136
+ >>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
137
+ >>> with PostgresSaver.from_conn_string(DB_URI) as memory:
138
+ ... # Run a graph, then list the checkpoints
139
+ >>> config = {"configurable": {"thread_id": "1"}}
140
+ >>> checkpoints = list(memory.list(config, limit=2))
141
+ >>> print(checkpoints)
142
+ [CheckpointTuple(...), CheckpointTuple(...)]
143
+
144
+ >>> config = {"configurable": {"thread_id": "1"}}
145
+ >>> before = {"configurable": {"checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875"}}
146
+ >>> with PostgresSaver.from_conn_string(DB_URI) as memory:
147
+ ... # Run a graph, then list the checkpoints
148
+ >>> checkpoints = list(memory.list(config, before=before))
149
+ >>> print(checkpoints)
150
+ [CheckpointTuple(...), ...]
151
+ """
152
+ where, args = self._search_where(config, filter, before)
153
+ query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
154
+ params = list(args)
155
+ if limit is not None:
156
+ query += " LIMIT %s"
157
+ params.append(int(limit))
158
+ # if we change this to use .stream() we need to make sure to close the cursor
159
+ with self._cursor() as cur:
160
+ cur.execute(query, params)
161
+ values = cur.fetchall()
162
+ if not values:
163
+ return
164
+ # migrate pending sends if necessary
165
+ if to_migrate := [
166
+ v
167
+ for v in values
168
+ if v["checkpoint"]["v"] < 4 and v["parent_checkpoint_id"]
169
+ ]:
170
+ cur.execute(
171
+ self.SELECT_PENDING_SENDS_SQL,
172
+ (
173
+ values[0]["thread_id"],
174
+ [v["parent_checkpoint_id"] for v in to_migrate],
175
+ ),
176
+ )
177
+ grouped_by_parent = defaultdict(list)
178
+ for value in to_migrate:
179
+ grouped_by_parent[value["parent_checkpoint_id"]].append(value)
180
+ for sends in cur:
181
+ for value in grouped_by_parent[sends["checkpoint_id"]]:
182
+ if value["channel_values"] is None:
183
+ value["channel_values"] = []
184
+ self._migrate_pending_sends(
185
+ sends["sends"],
186
+ value["checkpoint"],
187
+ value["channel_values"],
188
+ )
189
+ for value in values:
190
+ yield self._load_checkpoint_tuple(value)
191
+
192
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
193
+ """Get a checkpoint tuple from the database.
194
+
195
+ This method retrieves a checkpoint tuple from the Postgres database based on the
196
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
197
+ the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
198
+ for the given thread ID is retrieved.
199
+
200
+ Args:
201
+ config: The config to use for retrieving the checkpoint.
202
+
203
+ Returns:
204
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
205
+
206
+ Examples:
207
+
208
+ Basic:
209
+ >>> config = {"configurable": {"thread_id": "1"}}
210
+ >>> checkpoint_tuple = memory.get_tuple(config)
211
+ >>> print(checkpoint_tuple)
212
+ CheckpointTuple(...)
213
+
214
+ With timestamp:
215
+
216
+ >>> config = {
217
+ ... "configurable": {
218
+ ... "thread_id": "1",
219
+ ... "checkpoint_ns": "",
220
+ ... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
221
+ ... }
222
+ ... }
223
+ >>> checkpoint_tuple = memory.get_tuple(config)
224
+ >>> print(checkpoint_tuple)
225
+ CheckpointTuple(...)
226
+ """
227
+ thread_id = config["configurable"]["thread_id"]
228
+ checkpoint_id = get_checkpoint_id(config)
229
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
230
+ if checkpoint_id:
231
+ args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
232
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id = %s"
233
+ else:
234
+ args = (thread_id, checkpoint_ns)
235
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
236
+
237
+ with self._cursor() as cur:
238
+ cur.execute(
239
+ self.SELECT_SQL + where,
240
+ args,
241
+ )
242
+ value = cur.fetchone()
243
+ if value is None:
244
+ return None
245
+
246
+ # migrate pending sends if necessary
247
+ if value["checkpoint"]["v"] < 4 and value["parent_checkpoint_id"]:
248
+ cur.execute(
249
+ self.SELECT_PENDING_SENDS_SQL,
250
+ (thread_id, [value["parent_checkpoint_id"]]),
251
+ )
252
+ if sends := cur.fetchone():
253
+ if value["channel_values"] is None:
254
+ value["channel_values"] = []
255
+ self._migrate_pending_sends(
256
+ sends["sends"],
257
+ value["checkpoint"],
258
+ value["channel_values"],
259
+ )
260
+
261
+ return self._load_checkpoint_tuple(value)
262
+
263
+ def put(
264
+ self,
265
+ config: RunnableConfig,
266
+ checkpoint: Checkpoint,
267
+ metadata: CheckpointMetadata,
268
+ new_versions: ChannelVersions,
269
+ ) -> RunnableConfig:
270
+ """Save a checkpoint to the database.
271
+
272
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
273
+ with the provided config and its parent config (if any).
274
+
275
+ Args:
276
+ config: The config to associate with the checkpoint.
277
+ checkpoint: The checkpoint to save.
278
+ metadata: Additional metadata to save with the checkpoint.
279
+ new_versions: New channel versions as of this write.
280
+
281
+ Returns:
282
+ RunnableConfig: Updated configuration after storing the checkpoint.
283
+
284
+ Examples:
285
+
286
+ >>> from coco_graph.checkpoint.postgres import PostgresSaver
287
+ >>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
288
+ >>> with PostgresSaver.from_conn_string(DB_URI) as memory:
289
+ >>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
290
+ >>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "channel_values": {"key": "value"}}
291
+ >>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
292
+ >>> print(saved_config)
293
+ {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
294
+ """
295
+ configurable = config["configurable"].copy()
296
+ thread_id = configurable.pop("thread_id")
297
+ checkpoint_ns = configurable.pop("checkpoint_ns")
298
+ checkpoint_id = configurable.pop("checkpoint_id", None)
299
+ copy = checkpoint.copy()
300
+ copy["channel_values"] = copy["channel_values"].copy()
301
+ next_config = {
302
+ "configurable": {
303
+ "thread_id": thread_id,
304
+ "checkpoint_ns": checkpoint_ns,
305
+ "checkpoint_id": checkpoint["id"],
306
+ }
307
+ }
308
+
309
+ # inline primitive values in checkpoint table
310
+ # others are stored in blobs table
311
+ blob_values = {}
312
+ for k, v in checkpoint["channel_values"].items():
313
+ if isinstance(v, _DeltaSnapshot):
314
+ blob_values[k] = copy["channel_values"].pop(k)
315
+ copy["channel_values"][k] = True
316
+ elif v is None or isinstance(v, (str, int, float, bool)):
317
+ pass
318
+ else:
319
+ blob_values[k] = copy["channel_values"].pop(k)
320
+
321
+ with self._cursor(pipeline=True) as cur:
322
+ if blob_versions := {
323
+ k: v for k, v in new_versions.items() if k in blob_values
324
+ }:
325
+ cur.executemany(
326
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
327
+ self._dump_blobs(
328
+ thread_id,
329
+ checkpoint_ns,
330
+ blob_values,
331
+ blob_versions,
332
+ ),
333
+ )
334
+ cur.execute(
335
+ self.UPSERT_CHECKPOINTS_SQL,
336
+ (
337
+ thread_id,
338
+ checkpoint_ns,
339
+ checkpoint["id"],
340
+ checkpoint_id,
341
+ Jsonb(copy),
342
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
343
+ ),
344
+ )
345
+ return next_config
346
+
347
+ def put_writes(
348
+ self,
349
+ config: RunnableConfig,
350
+ writes: Sequence[tuple[str, Any]],
351
+ task_id: str,
352
+ task_path: str = "",
353
+ ) -> None:
354
+ """Store intermediate writes linked to a checkpoint.
355
+
356
+ This method saves intermediate writes associated with a checkpoint to the Postgres database.
357
+
358
+ Args:
359
+ config: Configuration of the related checkpoint.
360
+ writes: List of writes to store.
361
+ task_id: Identifier for the task creating the writes.
362
+ """
363
+ query = (
364
+ self.UPSERT_CHECKPOINT_WRITES_SQL
365
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
366
+ else self.INSERT_CHECKPOINT_WRITES_SQL
367
+ )
368
+ with self._cursor(pipeline=True) as cur:
369
+ cur.executemany(
370
+ query,
371
+ self._dump_writes(
372
+ config["configurable"]["thread_id"],
373
+ config["configurable"]["checkpoint_ns"],
374
+ config["configurable"]["checkpoint_id"],
375
+ task_id,
376
+ task_path,
377
+ writes,
378
+ ),
379
+ )
380
+
381
+ def delete_thread(self, thread_id: str) -> None:
382
+ """Delete all checkpoints and writes associated with a thread ID.
383
+
384
+ Args:
385
+ thread_id: The thread ID to delete.
386
+
387
+ Returns:
388
+ None
389
+ """
390
+ with self._cursor(pipeline=True) as cur:
391
+ cur.execute(
392
+ "DELETE FROM checkpoints WHERE thread_id = %s",
393
+ (str(thread_id),),
394
+ )
395
+ cur.execute(
396
+ "DELETE FROM checkpoint_blobs WHERE thread_id = %s",
397
+ (str(thread_id),),
398
+ )
399
+ cur.execute(
400
+ "DELETE FROM checkpoint_writes WHERE thread_id = %s",
401
+ (str(thread_id),),
402
+ )
403
+
404
+ @contextmanager
405
+ def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
406
+ """Create a database cursor as a context manager.
407
+
408
+ Args:
409
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
410
+ Will be applied regardless of whether the PostgresSaver instance was initialized with a pipeline.
411
+ If pipeline mode is not supported, will fall back to using transaction context manager.
412
+ """
413
+ with self.lock, _internal.get_connection(self.conn) as conn:
414
+ if self.pipe:
415
+ # a connection in pipeline mode can be used concurrently
416
+ # in multiple threads/coroutines, but only one cursor can be
417
+ # used at a time
418
+ try:
419
+ with conn.cursor(binary=True, row_factory=dict_row) as cur:
420
+ yield cur
421
+ finally:
422
+ if pipeline:
423
+ self.pipe.sync()
424
+ elif pipeline:
425
+ # a connection not in pipeline mode can only be used by one
426
+ # thread/coroutine at a time, so we acquire a lock
427
+ if self.supports_pipeline:
428
+ with (
429
+ conn.pipeline(),
430
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
431
+ ):
432
+ yield cur
433
+ else:
434
+ # Use connection's transaction context manager when pipeline mode not supported
435
+ with (
436
+ conn.transaction(),
437
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
438
+ ):
439
+ yield cur
440
+ else:
441
+ with conn.cursor(binary=True, row_factory=dict_row) as cur:
442
+ yield cur
443
+
444
+ def get_delta_channel_history(
445
+ self, *, config: RunnableConfig, channels: Sequence[str]
446
+ ) -> Mapping[str, DeltaChannelHistory]:
447
+ """Fast-path override of `BaseCheckpointSaver.get_delta_channel_history`.
448
+
449
+ Two-stage query, both stages cover ALL requested channels:
450
+
451
+ * Stage 1 (paged): dynamic SELECT over `checkpoints` with K parallel
452
+ JSONB key lookups (one column pair per channel) — no subquery, no
453
+ aggregation. Pages newest-first by `checkpoint_id` with a cursor;
454
+ page size is `_DELTA_PAGE_SIZE`. Stops paging when every channel
455
+ has found its seed or the chain is exhausted.
456
+
457
+ * Stage 2 (per-channel UNION ALL): one branch per channel reading
458
+ `checkpoint_writes` filtered to that channel's specific
459
+ `chain_cids`, plus one branch per channel that has a seed reading
460
+ `checkpoint_blobs` for that channel + version. Avoids the
461
+ over-fetch of a single `channel = ANY(channels)` filter when
462
+ channels have different chain depths.
463
+ """
464
+ if not channels:
465
+ return {}
466
+ channels = list(channels)
467
+ thread_id = config["configurable"]["thread_id"]
468
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
469
+ checkpoint_id = get_checkpoint_id(config)
470
+ if checkpoint_id is None:
471
+ target = self.get_tuple(config)
472
+ if target is None:
473
+ return {ch: {"writes": []} for ch in channels}
474
+ checkpoint_id = target.config["configurable"]["checkpoint_id"]
475
+
476
+ # Stage 1: paged K-JSONB-lookup scan, walking the parent chain in
477
+ # Python after each page. Stops as soon as every channel has its seed.
478
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
479
+ parent_of: dict[str, str | None] = {}
480
+ ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
481
+ hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
482
+ inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
483
+ chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
484
+ seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
485
+ seed_inline_by_ch: dict[str, Any] = {}
486
+ walk_cursor_by_ch: dict[str, str | None] = {}
487
+ seeded: set[str] = set()
488
+ cursor: str | None = None
489
+
490
+ with self._cursor() as cur:
491
+ while True:
492
+ stage1_params: list[Any] = []
493
+ for ch in channels:
494
+ # ver_i, blob channel, blob version, inline_i
495
+ stage1_params.extend([ch, ch, ch, ch])
496
+ stage1_params.extend(
497
+ [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
498
+ )
499
+ cur.execute(stage1_sql, stage1_params)
500
+ page = cur.fetchall()
501
+ if not page:
502
+ break
503
+ oldest = self._ingest_stage1_page(
504
+ cast("list[Mapping[str, Any]]", page),
505
+ channels,
506
+ parent_of,
507
+ ver_by_i_by_cid,
508
+ hb_by_i_by_cid,
509
+ inline_by_i_by_cid,
510
+ )
511
+ self._try_advance_walks(
512
+ checkpoint_id,
513
+ channels,
514
+ parent_of,
515
+ ver_by_i_by_cid,
516
+ hb_by_i_by_cid,
517
+ inline_by_i_by_cid,
518
+ chain_by_ch,
519
+ seed_ver_by_ch,
520
+ seed_inline_by_ch,
521
+ walk_cursor_by_ch,
522
+ seeded,
523
+ )
524
+ # Stop if every channel is seeded, or the page was short
525
+ # (chain exhausted — no more rows to fetch).
526
+ if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
527
+ break
528
+ cursor = oldest
529
+
530
+ # Stage 2: per-channel UNION ALL — one writes branch per channel
531
+ # with non-empty chain, plus one blob branch per seeded channel.
532
+ channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
533
+ channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
534
+ stage2_sql = _build_delta_stage2_sql(
535
+ channels_with_chain=channels_with_chain,
536
+ channels_with_seed=channels_with_seed,
537
+ )
538
+
539
+ if stage2_sql:
540
+ stage2_params: list[Any] = []
541
+ for ch in channels_with_chain:
542
+ stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
543
+ for ch in channels_with_seed:
544
+ stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
545
+ with self._cursor() as cur:
546
+ cur.execute(stage2_sql, stage2_params)
547
+ stage2_rows = cur.fetchall()
548
+ else:
549
+ stage2_rows = []
550
+
551
+ return self._build_delta_channels_writes_history(
552
+ channels=channels,
553
+ chain_by_ch=chain_by_ch,
554
+ seed_ver_by_ch=seed_ver_by_ch,
555
+ seed_inline_by_ch=seed_inline_by_ch,
556
+ stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
557
+ )
558
+
559
+ def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
560
+ """
561
+ Convert a database row into a CheckpointTuple object.
562
+
563
+ Args:
564
+ value: A row from the database containing checkpoint data.
565
+
566
+ Returns:
567
+ CheckpointTuple: A structured representation of the checkpoint,
568
+ including its configuration, metadata, parent checkpoint (if any),
569
+ and pending writes.
570
+ """
571
+ return CheckpointTuple(
572
+ {
573
+ "configurable": {
574
+ "thread_id": value["thread_id"],
575
+ "checkpoint_ns": value["checkpoint_ns"],
576
+ "checkpoint_id": value["checkpoint_id"],
577
+ }
578
+ },
579
+ {
580
+ **value["checkpoint"],
581
+ "channel_values": {
582
+ **(value["checkpoint"].get("channel_values") or {}),
583
+ **self._load_blobs(value["channel_values"]),
584
+ },
585
+ },
586
+ value["metadata"],
587
+ (
588
+ {
589
+ "configurable": {
590
+ "thread_id": value["thread_id"],
591
+ "checkpoint_ns": value["checkpoint_ns"],
592
+ "checkpoint_id": value["parent_checkpoint_id"],
593
+ }
594
+ }
595
+ if value["parent_checkpoint_id"]
596
+ else None
597
+ ),
598
+ self._load_writes(value["pending_writes"]),
599
+ )
600
+
601
+
602
+ __all__ = ["PostgresSaver", "BasePostgresSaver", "ShallowPostgresSaver", "Conn"]
@@ -0,0 +1,23 @@
1
+ """Shared async utility functions for the Postgres checkpoint & storage classes."""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+
6
+ from psycopg import AsyncConnection
7
+ from psycopg.rows import DictRow
8
+ from psycopg_pool import AsyncConnectionPool
9
+
10
+ Conn = AsyncConnection[DictRow] | AsyncConnectionPool[AsyncConnection[DictRow]]
11
+
12
+
13
+ @asynccontextmanager
14
+ async def get_connection(
15
+ conn: Conn,
16
+ ) -> AsyncIterator[AsyncConnection[DictRow]]:
17
+ if isinstance(conn, AsyncConnection):
18
+ yield conn
19
+ elif isinstance(conn, AsyncConnectionPool):
20
+ async with conn.connection() as conn:
21
+ yield conn
22
+ else:
23
+ raise TypeError(f"Invalid connection type: {type(conn)}")
@@ -0,0 +1,21 @@
1
+ """Shared utility functions for the Postgres checkpoint & storage classes."""
2
+
3
+ from collections.abc import Iterator
4
+ from contextlib import contextmanager
5
+
6
+ from psycopg import Connection
7
+ from psycopg.rows import DictRow
8
+ from psycopg_pool import ConnectionPool
9
+
10
+ Conn = Connection[DictRow] | ConnectionPool[Connection[DictRow]]
11
+
12
+
13
+ @contextmanager
14
+ def get_connection(conn: Conn) -> Iterator[Connection[DictRow]]:
15
+ if isinstance(conn, Connection):
16
+ yield conn
17
+ elif isinstance(conn, ConnectionPool):
18
+ with conn.connection() as conn:
19
+ yield conn
20
+ else:
21
+ raise TypeError(f"Invalid connection type: {type(conn)}")