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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import orjson
|
|
11
|
+
from coco_graph.store.base import (
|
|
12
|
+
GetOp,
|
|
13
|
+
ListNamespacesOp,
|
|
14
|
+
Op,
|
|
15
|
+
PutOp,
|
|
16
|
+
Result,
|
|
17
|
+
SearchOp,
|
|
18
|
+
)
|
|
19
|
+
from coco_graph.store.base.batch import AsyncBatchedBaseStore
|
|
20
|
+
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
21
|
+
from psycopg.rows import DictRow, dict_row
|
|
22
|
+
from psycopg_pool import AsyncConnectionPool
|
|
23
|
+
|
|
24
|
+
from coco_graph.checkpoint.postgres import _ainternal
|
|
25
|
+
from coco_graph.store.postgres.base import (
|
|
26
|
+
PLACEHOLDER,
|
|
27
|
+
BasePostgresStore,
|
|
28
|
+
PoolConfig,
|
|
29
|
+
PostgresIndexConfig,
|
|
30
|
+
Row,
|
|
31
|
+
TTLConfig,
|
|
32
|
+
_decode_ns_bytes,
|
|
33
|
+
_ensure_index_config,
|
|
34
|
+
_group_ops,
|
|
35
|
+
_row_to_item,
|
|
36
|
+
_row_to_search_item,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
|
|
43
|
+
"""Asynchronous Postgres-backed store with optional vector search using pgvector.
|
|
44
|
+
|
|
45
|
+
!!! example "Examples"
|
|
46
|
+
Basic setup and usage:
|
|
47
|
+
```python
|
|
48
|
+
from coco_graph.store.postgres import AsyncPostgresStore
|
|
49
|
+
|
|
50
|
+
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
|
51
|
+
|
|
52
|
+
async with AsyncPostgresStore.from_conn_string(conn_string) as store:
|
|
53
|
+
await store.setup() # Run migrations. Done once
|
|
54
|
+
|
|
55
|
+
# Store and retrieve data
|
|
56
|
+
await store.aput(("users", "123"), "prefs", {"theme": "dark"})
|
|
57
|
+
item = await store.aget(("users", "123"), "prefs")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Vector search using Coco embeddings:
|
|
61
|
+
```python
|
|
62
|
+
from coco.embeddings import init_embeddings
|
|
63
|
+
from coco_graph.store.postgres import AsyncPostgresStore
|
|
64
|
+
|
|
65
|
+
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
|
66
|
+
|
|
67
|
+
async with AsyncPostgresStore.from_conn_string(
|
|
68
|
+
conn_string,
|
|
69
|
+
index={
|
|
70
|
+
"dims": 1536,
|
|
71
|
+
"embed": init_embeddings("openai:text-embedding-3-small"),
|
|
72
|
+
"fields": ["text"] # specify which fields to embed. Default is the whole serialized value
|
|
73
|
+
}
|
|
74
|
+
) as store:
|
|
75
|
+
await store.setup() # Run migrations. Done once
|
|
76
|
+
|
|
77
|
+
# Store documents
|
|
78
|
+
await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
|
|
79
|
+
await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
|
|
80
|
+
await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
|
|
81
|
+
|
|
82
|
+
# Search by similarity
|
|
83
|
+
results = await store.asearch(("docs",), query="programming guides", limit=2)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Using connection pooling for better performance:
|
|
87
|
+
```python
|
|
88
|
+
from coco_graph.store.postgres import AsyncPostgresStore, PoolConfig
|
|
89
|
+
|
|
90
|
+
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
|
91
|
+
|
|
92
|
+
async with AsyncPostgresStore.from_conn_string(
|
|
93
|
+
conn_string,
|
|
94
|
+
pool_config=PoolConfig(
|
|
95
|
+
min_size=5,
|
|
96
|
+
max_size=20
|
|
97
|
+
)
|
|
98
|
+
) as store:
|
|
99
|
+
await store.setup() # Run migrations. Done once
|
|
100
|
+
# Use store with connection pooling...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Warning:
|
|
104
|
+
Make sure to:
|
|
105
|
+
1. Call `setup()` before first use to create necessary tables and indexes
|
|
106
|
+
2. Have the pgvector extension available to use vector search
|
|
107
|
+
3. Use Python 3.10+ for async functionality
|
|
108
|
+
|
|
109
|
+
Note:
|
|
110
|
+
Semantic search is disabled by default. You can enable it by providing an `index` configuration
|
|
111
|
+
when creating the store. Without this configuration, all `index` arguments passed to
|
|
112
|
+
`put` or `aput` will have no effect.
|
|
113
|
+
|
|
114
|
+
Note:
|
|
115
|
+
If you provide a TTL configuration, you must explicitly call `start_ttl_sweeper()` to begin
|
|
116
|
+
the background task that removes expired items. Call `stop_ttl_sweeper()` to properly
|
|
117
|
+
clean up resources when you're done with the store.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
__slots__ = (
|
|
121
|
+
"_deserializer",
|
|
122
|
+
"pipe",
|
|
123
|
+
"lock",
|
|
124
|
+
"supports_pipeline",
|
|
125
|
+
"index_config",
|
|
126
|
+
"embeddings",
|
|
127
|
+
"ttl_config",
|
|
128
|
+
"_ttl_sweeper_task",
|
|
129
|
+
"_ttl_stop_event",
|
|
130
|
+
)
|
|
131
|
+
supports_ttl: bool = True
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
conn: _ainternal.Conn,
|
|
136
|
+
*,
|
|
137
|
+
pipe: AsyncPipeline | None = None,
|
|
138
|
+
deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
|
|
139
|
+
index: PostgresIndexConfig | None = None,
|
|
140
|
+
ttl: TTLConfig | None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
|
|
145
|
+
)
|
|
146
|
+
super().__init__()
|
|
147
|
+
self._deserializer = deserializer
|
|
148
|
+
self.conn = conn
|
|
149
|
+
self.pipe = pipe
|
|
150
|
+
self.lock = asyncio.Lock()
|
|
151
|
+
self.loop = asyncio.get_running_loop()
|
|
152
|
+
self.supports_pipeline = Capabilities().has_pipeline()
|
|
153
|
+
self.index_config = index
|
|
154
|
+
if self.index_config:
|
|
155
|
+
self.embeddings, self.index_config = _ensure_index_config(self.index_config)
|
|
156
|
+
else:
|
|
157
|
+
self.embeddings = None
|
|
158
|
+
|
|
159
|
+
self.ttl_config = ttl
|
|
160
|
+
self._ttl_sweeper_task: asyncio.Task[None] | None = None
|
|
161
|
+
self._ttl_stop_event = asyncio.Event()
|
|
162
|
+
|
|
163
|
+
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
|
164
|
+
grouped_ops, num_ops = _group_ops(ops)
|
|
165
|
+
results: list[Result] = [None] * num_ops
|
|
166
|
+
|
|
167
|
+
if self.pipe:
|
|
168
|
+
async with self.pipe:
|
|
169
|
+
await self._execute_batch(grouped_ops, results)
|
|
170
|
+
else:
|
|
171
|
+
await self._execute_batch(grouped_ops, results)
|
|
172
|
+
|
|
173
|
+
return results
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
@asynccontextmanager
|
|
177
|
+
async def from_conn_string(
|
|
178
|
+
cls,
|
|
179
|
+
conn_string: str,
|
|
180
|
+
*,
|
|
181
|
+
pipeline: bool = False,
|
|
182
|
+
pool_config: PoolConfig | None = None,
|
|
183
|
+
index: PostgresIndexConfig | None = None,
|
|
184
|
+
ttl: TTLConfig | None = None,
|
|
185
|
+
) -> AsyncIterator[AsyncPostgresStore]:
|
|
186
|
+
"""Create a new AsyncPostgresStore instance from a connection string.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
conn_string: The Postgres connection info string.
|
|
190
|
+
pipeline: Whether to use AsyncPipeline (only for single connections)
|
|
191
|
+
pool_config: Configuration for the connection pool.
|
|
192
|
+
If provided, will create a connection pool and use it instead of a single connection.
|
|
193
|
+
This overrides the `pipeline` argument.
|
|
194
|
+
index: The embedding config.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
AsyncPostgresStore: A new AsyncPostgresStore instance.
|
|
198
|
+
"""
|
|
199
|
+
if pool_config is not None:
|
|
200
|
+
pc = pool_config.copy()
|
|
201
|
+
async with cast(
|
|
202
|
+
AsyncConnectionPool[AsyncConnection[DictRow]],
|
|
203
|
+
AsyncConnectionPool(
|
|
204
|
+
conn_string,
|
|
205
|
+
min_size=pc.pop("min_size", 1),
|
|
206
|
+
max_size=pc.pop("max_size", None),
|
|
207
|
+
kwargs={
|
|
208
|
+
"autocommit": True,
|
|
209
|
+
"prepare_threshold": 0,
|
|
210
|
+
"row_factory": dict_row,
|
|
211
|
+
**(pc.pop("kwargs", None) or {}),
|
|
212
|
+
},
|
|
213
|
+
**cast(dict, pc),
|
|
214
|
+
),
|
|
215
|
+
) as pool:
|
|
216
|
+
yield cls(conn=pool, index=index, ttl=ttl)
|
|
217
|
+
else:
|
|
218
|
+
async with await AsyncConnection.connect(
|
|
219
|
+
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
|
220
|
+
) as conn:
|
|
221
|
+
if pipeline:
|
|
222
|
+
async with conn.pipeline() as pipe:
|
|
223
|
+
yield cls(conn=conn, pipe=pipe, index=index, ttl=ttl)
|
|
224
|
+
else:
|
|
225
|
+
yield cls(conn=conn, index=index, ttl=ttl)
|
|
226
|
+
|
|
227
|
+
async def setup(self) -> None:
|
|
228
|
+
"""Set up the store database asynchronously.
|
|
229
|
+
|
|
230
|
+
This method creates the necessary tables in the Postgres database if they don't
|
|
231
|
+
already exist and runs database migrations. It MUST be called directly by the user
|
|
232
|
+
the first time the store is used.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
async def _get_version(cur: AsyncCursor[DictRow], table: str) -> int:
|
|
236
|
+
await cur.execute(
|
|
237
|
+
f"""
|
|
238
|
+
CREATE TABLE IF NOT EXISTS {table} (
|
|
239
|
+
v INTEGER PRIMARY KEY
|
|
240
|
+
)
|
|
241
|
+
"""
|
|
242
|
+
)
|
|
243
|
+
await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
|
244
|
+
row = cast(dict, await cur.fetchone())
|
|
245
|
+
if row is None:
|
|
246
|
+
version = -1
|
|
247
|
+
else:
|
|
248
|
+
version = row["v"]
|
|
249
|
+
return version
|
|
250
|
+
|
|
251
|
+
async with self._cursor() as cur:
|
|
252
|
+
version = await _get_version(cur, table="store_migrations")
|
|
253
|
+
for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
|
|
254
|
+
await cur.execute(sql)
|
|
255
|
+
await cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
|
|
256
|
+
|
|
257
|
+
if self.index_config:
|
|
258
|
+
version = await _get_version(cur, table="vector_migrations")
|
|
259
|
+
for v, migration in enumerate(
|
|
260
|
+
self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
|
|
261
|
+
):
|
|
262
|
+
sql = migration.sql
|
|
263
|
+
if migration.params:
|
|
264
|
+
params = {
|
|
265
|
+
k: v(self) if v is not None and callable(v) else v
|
|
266
|
+
for k, v in migration.params.items()
|
|
267
|
+
}
|
|
268
|
+
if "dims" in params:
|
|
269
|
+
try:
|
|
270
|
+
params["dims"] = int(params["dims"])
|
|
271
|
+
except Exception as e:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"Invalid dims for vector index: {params['dims']}"
|
|
274
|
+
) from e
|
|
275
|
+
if "vector_type" in params:
|
|
276
|
+
vt = str(params["vector_type"])
|
|
277
|
+
if vt not in ("vector", "halfvec"):
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"Invalid vector_type for pgvector: {vt}"
|
|
280
|
+
)
|
|
281
|
+
params["vector_type"] = vt
|
|
282
|
+
if "index_type" in params:
|
|
283
|
+
it = str(params["index_type"])
|
|
284
|
+
if it not in ("hnsw", "ivfflat"):
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"Invalid index_type for pgvector: {it}"
|
|
287
|
+
)
|
|
288
|
+
params["index_type"] = it
|
|
289
|
+
sql = sql % params
|
|
290
|
+
await cur.execute(sql)
|
|
291
|
+
await cur.execute(
|
|
292
|
+
"INSERT INTO vector_migrations (v) VALUES (%s)", (v,)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
async def sweep_ttl(self) -> int:
|
|
296
|
+
"""Delete expired store items based on TTL.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
int: The number of deleted items.
|
|
300
|
+
"""
|
|
301
|
+
async with self._cursor() as cur:
|
|
302
|
+
await cur.execute(
|
|
303
|
+
"""
|
|
304
|
+
DELETE FROM store
|
|
305
|
+
WHERE expires_at IS NOT NULL AND expires_at < NOW()
|
|
306
|
+
"""
|
|
307
|
+
)
|
|
308
|
+
deleted_count = cur.rowcount
|
|
309
|
+
return deleted_count
|
|
310
|
+
|
|
311
|
+
async def start_ttl_sweeper(
|
|
312
|
+
self, sweep_interval_minutes: int | None = None
|
|
313
|
+
) -> asyncio.Task[None]:
|
|
314
|
+
"""Periodically delete expired store items based on TTL.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Task that can be awaited or cancelled.
|
|
318
|
+
"""
|
|
319
|
+
if not self.ttl_config:
|
|
320
|
+
return asyncio.create_task(asyncio.sleep(0))
|
|
321
|
+
|
|
322
|
+
if self._ttl_sweeper_task is not None and not self._ttl_sweeper_task.done():
|
|
323
|
+
return self._ttl_sweeper_task
|
|
324
|
+
|
|
325
|
+
self._ttl_stop_event.clear()
|
|
326
|
+
|
|
327
|
+
interval = float(
|
|
328
|
+
sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
|
|
329
|
+
)
|
|
330
|
+
logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
|
|
331
|
+
|
|
332
|
+
async def _sweep_loop() -> None:
|
|
333
|
+
while not self._ttl_stop_event.is_set():
|
|
334
|
+
try:
|
|
335
|
+
try:
|
|
336
|
+
await asyncio.wait_for(
|
|
337
|
+
self._ttl_stop_event.wait(),
|
|
338
|
+
timeout=interval * 60,
|
|
339
|
+
)
|
|
340
|
+
break
|
|
341
|
+
except asyncio.TimeoutError:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
expired_items = await self.sweep_ttl()
|
|
345
|
+
if expired_items > 0:
|
|
346
|
+
logger.info(f"Store swept {expired_items} expired items")
|
|
347
|
+
except asyncio.CancelledError:
|
|
348
|
+
break
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
logger.exception("Store TTL sweep iteration failed", exc_info=exc)
|
|
351
|
+
|
|
352
|
+
task = asyncio.create_task(_sweep_loop())
|
|
353
|
+
task.set_name("ttl_sweeper")
|
|
354
|
+
self._ttl_sweeper_task = task
|
|
355
|
+
return task
|
|
356
|
+
|
|
357
|
+
async def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
|
|
358
|
+
"""Stop the TTL sweeper task if it's running.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
timeout: Maximum time to wait for the task to stop, in seconds.
|
|
362
|
+
If `None`, wait indefinitely.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
bool: True if the task was successfully stopped or wasn't running,
|
|
366
|
+
False if the timeout was reached before the task stopped.
|
|
367
|
+
"""
|
|
368
|
+
if self._ttl_sweeper_task is None or self._ttl_sweeper_task.done():
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
logger.info("Stopping TTL sweeper task")
|
|
372
|
+
self._ttl_stop_event.set()
|
|
373
|
+
|
|
374
|
+
if timeout is not None:
|
|
375
|
+
try:
|
|
376
|
+
await asyncio.wait_for(self._ttl_sweeper_task, timeout=timeout)
|
|
377
|
+
success = True
|
|
378
|
+
except asyncio.TimeoutError:
|
|
379
|
+
success = False
|
|
380
|
+
else:
|
|
381
|
+
await self._ttl_sweeper_task
|
|
382
|
+
success = True
|
|
383
|
+
|
|
384
|
+
if success:
|
|
385
|
+
self._ttl_sweeper_task = None
|
|
386
|
+
logger.info("TTL sweeper task stopped")
|
|
387
|
+
else:
|
|
388
|
+
logger.warning("Timed out waiting for TTL sweeper task to stop")
|
|
389
|
+
|
|
390
|
+
return success
|
|
391
|
+
|
|
392
|
+
async def __aenter__(self) -> AsyncPostgresStore:
|
|
393
|
+
return self
|
|
394
|
+
|
|
395
|
+
async def __aexit__(
|
|
396
|
+
self,
|
|
397
|
+
exc_type: type[BaseException] | None,
|
|
398
|
+
exc_val: BaseException | None,
|
|
399
|
+
exc_tb: TracebackType | None,
|
|
400
|
+
) -> None:
|
|
401
|
+
# Ensure the TTL sweeper task is stopped when exiting the context
|
|
402
|
+
if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
|
|
403
|
+
# Set the event to signal the task to stop
|
|
404
|
+
self._ttl_stop_event.set()
|
|
405
|
+
# We don't wait for the task to complete here to avoid blocking
|
|
406
|
+
# The task will clean up itself gracefully
|
|
407
|
+
|
|
408
|
+
async def _execute_batch(
|
|
409
|
+
self,
|
|
410
|
+
grouped_ops: dict,
|
|
411
|
+
results: list[Result],
|
|
412
|
+
conn: AsyncConnection[DictRow] | None = None,
|
|
413
|
+
) -> None:
|
|
414
|
+
# Keep `conn` for compatibility with subclasses overriding this private hook.
|
|
415
|
+
# All database I/O goes through `_cursor()`, which owns connection acquisition.
|
|
416
|
+
async with self._cursor(pipeline=True) as cur:
|
|
417
|
+
if GetOp in grouped_ops:
|
|
418
|
+
await self._batch_get_ops(
|
|
419
|
+
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
|
|
420
|
+
results,
|
|
421
|
+
cur,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if SearchOp in grouped_ops:
|
|
425
|
+
await self._batch_search_ops(
|
|
426
|
+
cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
|
|
427
|
+
results,
|
|
428
|
+
cur,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if ListNamespacesOp in grouped_ops:
|
|
432
|
+
await self._batch_list_namespaces_ops(
|
|
433
|
+
cast(
|
|
434
|
+
Sequence[tuple[int, ListNamespacesOp]],
|
|
435
|
+
grouped_ops[ListNamespacesOp],
|
|
436
|
+
),
|
|
437
|
+
results,
|
|
438
|
+
cur,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if PutOp in grouped_ops:
|
|
442
|
+
await self._batch_put_ops(
|
|
443
|
+
cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]),
|
|
444
|
+
cur,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
async def _batch_get_ops(
|
|
448
|
+
self,
|
|
449
|
+
get_ops: Sequence[tuple[int, GetOp]],
|
|
450
|
+
results: list[Result],
|
|
451
|
+
cur: AsyncCursor[DictRow],
|
|
452
|
+
) -> None:
|
|
453
|
+
for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
|
|
454
|
+
await cur.execute(query, params)
|
|
455
|
+
rows = cast(list[Row], await cur.fetchall())
|
|
456
|
+
key_to_row = {row["key"]: row for row in rows}
|
|
457
|
+
for idx, key in items:
|
|
458
|
+
row = key_to_row.get(key)
|
|
459
|
+
if row:
|
|
460
|
+
results[idx] = _row_to_item(
|
|
461
|
+
namespace, row, loader=self._deserializer
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
results[idx] = None
|
|
465
|
+
|
|
466
|
+
async def _batch_put_ops(
|
|
467
|
+
self,
|
|
468
|
+
put_ops: Sequence[tuple[int, PutOp]],
|
|
469
|
+
cur: AsyncCursor[DictRow],
|
|
470
|
+
) -> None:
|
|
471
|
+
queries, embedding_request = self._prepare_batch_PUT_queries(put_ops)
|
|
472
|
+
if embedding_request:
|
|
473
|
+
if self.embeddings is None:
|
|
474
|
+
# Should not get here since the embedding config is required
|
|
475
|
+
# to return an embedding_request above
|
|
476
|
+
raise ValueError(
|
|
477
|
+
"Embedding configuration is required for vector operations "
|
|
478
|
+
f"(for semantic search). "
|
|
479
|
+
f"Please provide an EmbeddingConfig when initializing the {self.__class__.__name__}."
|
|
480
|
+
)
|
|
481
|
+
query, txt_params = embedding_request
|
|
482
|
+
vectors = await self.embeddings.aembed_documents(
|
|
483
|
+
[param[-1] for param in txt_params]
|
|
484
|
+
)
|
|
485
|
+
queries.append(
|
|
486
|
+
(
|
|
487
|
+
query,
|
|
488
|
+
[
|
|
489
|
+
p
|
|
490
|
+
for (ns, k, pathname, _), vector in zip(
|
|
491
|
+
txt_params, vectors, strict=False
|
|
492
|
+
)
|
|
493
|
+
for p in (ns, k, pathname, vector)
|
|
494
|
+
],
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
for query, params in queries:
|
|
499
|
+
await cur.execute(query, params)
|
|
500
|
+
|
|
501
|
+
async def _batch_search_ops(
|
|
502
|
+
self,
|
|
503
|
+
search_ops: Sequence[tuple[int, SearchOp]],
|
|
504
|
+
results: list[Result],
|
|
505
|
+
cur: AsyncCursor[DictRow],
|
|
506
|
+
) -> None:
|
|
507
|
+
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
|
508
|
+
|
|
509
|
+
if embedding_requests and self.embeddings:
|
|
510
|
+
vectors = await self.embeddings.aembed_documents(
|
|
511
|
+
[query for _, query in embedding_requests]
|
|
512
|
+
)
|
|
513
|
+
for (idx, _), vector in zip(embedding_requests, vectors, strict=False):
|
|
514
|
+
_paramslist = queries[idx][1]
|
|
515
|
+
for i in range(len(_paramslist)):
|
|
516
|
+
if _paramslist[i] is PLACEHOLDER:
|
|
517
|
+
_paramslist[i] = vector
|
|
518
|
+
|
|
519
|
+
for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
|
|
520
|
+
await cur.execute(query, params)
|
|
521
|
+
rows = cast(list[Row], await cur.fetchall())
|
|
522
|
+
items = [
|
|
523
|
+
_row_to_search_item(
|
|
524
|
+
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
|
|
525
|
+
)
|
|
526
|
+
for row in rows
|
|
527
|
+
]
|
|
528
|
+
results[idx] = items
|
|
529
|
+
|
|
530
|
+
async def _batch_list_namespaces_ops(
|
|
531
|
+
self,
|
|
532
|
+
list_ops: Sequence[tuple[int, ListNamespacesOp]],
|
|
533
|
+
results: list[Result],
|
|
534
|
+
cur: AsyncCursor[DictRow],
|
|
535
|
+
) -> None:
|
|
536
|
+
queries = self._get_batch_list_namespaces_queries(list_ops)
|
|
537
|
+
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
|
538
|
+
await cur.execute(query, params)
|
|
539
|
+
rows = cast(list[dict], await cur.fetchall())
|
|
540
|
+
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
|
|
541
|
+
results[idx] = namespaces
|
|
542
|
+
|
|
543
|
+
@asynccontextmanager
|
|
544
|
+
async def _cursor(
|
|
545
|
+
self, *, pipeline: bool = False
|
|
546
|
+
) -> AsyncIterator[AsyncCursor[DictRow]]:
|
|
547
|
+
"""Create a database cursor as a context manager.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
pipeline: whether to use pipeline for the DB operations inside the context manager.
|
|
551
|
+
Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
|
|
552
|
+
If pipeline mode is not supported, will fall back to using transaction context manager.
|
|
553
|
+
"""
|
|
554
|
+
is_pooled_conn = isinstance(self.conn, AsyncConnectionPool)
|
|
555
|
+
# With AsyncConnectionPool, each _cursor() call checks out its own connection.
|
|
556
|
+
# The pool does not hand out the same connection concurrently, so a shared lock
|
|
557
|
+
# across calls is unnecessary here.
|
|
558
|
+
lock = asyncio.Lock() if is_pooled_conn else self.lock
|
|
559
|
+
async with _ainternal.get_connection(self.conn) as conn:
|
|
560
|
+
if self.pipe:
|
|
561
|
+
# a connection in pipeline mode can be used concurrently
|
|
562
|
+
# in multiple threads/coroutines, but only one cursor can be
|
|
563
|
+
# used at a time
|
|
564
|
+
try:
|
|
565
|
+
async with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
566
|
+
yield cur
|
|
567
|
+
finally:
|
|
568
|
+
if pipeline:
|
|
569
|
+
await self.pipe.sync()
|
|
570
|
+
elif pipeline:
|
|
571
|
+
# a connection not in pipeline mode can only be used by one
|
|
572
|
+
# thread/coroutine at a time, so we acquire a lock
|
|
573
|
+
if self.supports_pipeline:
|
|
574
|
+
async with (
|
|
575
|
+
lock,
|
|
576
|
+
conn.pipeline(),
|
|
577
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
578
|
+
):
|
|
579
|
+
yield cur
|
|
580
|
+
else:
|
|
581
|
+
async with (
|
|
582
|
+
lock,
|
|
583
|
+
conn.transaction(),
|
|
584
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
585
|
+
):
|
|
586
|
+
yield cur
|
|
587
|
+
else:
|
|
588
|
+
async with (
|
|
589
|
+
lock,
|
|
590
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
591
|
+
):
|
|
592
|
+
yield cur
|