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,19 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from coco_graph.types import _DC_KWARGS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass(**_DC_KWARGS)
|
|
9
|
+
class PregelScratchpad:
|
|
10
|
+
step: int
|
|
11
|
+
stop: int
|
|
12
|
+
# call
|
|
13
|
+
call_counter: Callable[[], int]
|
|
14
|
+
# interrupt
|
|
15
|
+
interrupt_counter: Callable[[], int]
|
|
16
|
+
get_null_resume: Callable[[bool], Any]
|
|
17
|
+
resume: list[Any]
|
|
18
|
+
# subgraph
|
|
19
|
+
subgraph_counter: Callable[[], int]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import types
|
|
7
|
+
from collections import deque
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import (
|
|
10
|
+
Annotated,
|
|
11
|
+
Any,
|
|
12
|
+
Literal,
|
|
13
|
+
Union,
|
|
14
|
+
get_args,
|
|
15
|
+
get_origin,
|
|
16
|
+
get_type_hints,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from coco_core import messages as lc_messages
|
|
20
|
+
from coco_graph.checkpoint.base import BaseCheckpointSaver
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
from typing_extensions import NotRequired, Required, is_typeddict
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from coco_graph.checkpoint.serde._msgpack import (
|
|
26
|
+
STRICT_MSGPACK_ENABLED,
|
|
27
|
+
)
|
|
28
|
+
except ImportError:
|
|
29
|
+
STRICT_MSGPACK_ENABLED = False
|
|
30
|
+
|
|
31
|
+
_warned_allowlist_unsupported = False
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _supports_checkpointer_allowlist() -> bool:
|
|
37
|
+
return hasattr(BaseCheckpointSaver, "with_allowlist")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_SUPPORTS_ALLOWLIST = _supports_checkpointer_allowlist()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def apply_checkpointer_allowlist(
|
|
44
|
+
checkpointer: Any, allowlist: set[tuple[str, ...]] | None
|
|
45
|
+
) -> Any:
|
|
46
|
+
if not checkpointer or allowlist is None or checkpointer in (True, False):
|
|
47
|
+
return checkpointer
|
|
48
|
+
if not _SUPPORTS_ALLOWLIST:
|
|
49
|
+
global _warned_allowlist_unsupported
|
|
50
|
+
if not _warned_allowlist_unsupported:
|
|
51
|
+
logger.warning(
|
|
52
|
+
"Checkpointer does not support with_allowlist; strict msgpack "
|
|
53
|
+
"allowlist will be skipped."
|
|
54
|
+
)
|
|
55
|
+
_warned_allowlist_unsupported = True
|
|
56
|
+
return checkpointer
|
|
57
|
+
return checkpointer.with_allowlist(allowlist)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def curated_core_allowlist() -> set[tuple[str, ...]]:
|
|
61
|
+
allowlist: set[tuple[str, ...]] = set()
|
|
62
|
+
for name in (
|
|
63
|
+
"BaseMessage",
|
|
64
|
+
"BaseMessageChunk",
|
|
65
|
+
"HumanMessage",
|
|
66
|
+
"HumanMessageChunk",
|
|
67
|
+
"AIMessage",
|
|
68
|
+
"AIMessageChunk",
|
|
69
|
+
"SystemMessage",
|
|
70
|
+
"SystemMessageChunk",
|
|
71
|
+
"ChatMessage",
|
|
72
|
+
"ChatMessageChunk",
|
|
73
|
+
"ToolMessage",
|
|
74
|
+
"ToolMessageChunk",
|
|
75
|
+
"FunctionMessage",
|
|
76
|
+
"FunctionMessageChunk",
|
|
77
|
+
"RemoveMessage",
|
|
78
|
+
):
|
|
79
|
+
cls = getattr(lc_messages, name, None)
|
|
80
|
+
if cls is None:
|
|
81
|
+
continue
|
|
82
|
+
allowlist.add((cls.__module__, cls.__name__))
|
|
83
|
+
|
|
84
|
+
return allowlist
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_serde_allowlist(
|
|
88
|
+
*,
|
|
89
|
+
schemas: list[type[Any]] | None = None,
|
|
90
|
+
channels: dict[str, Any] | None = None,
|
|
91
|
+
) -> set[tuple[str, ...]]:
|
|
92
|
+
allowlist = curated_core_allowlist()
|
|
93
|
+
if schemas:
|
|
94
|
+
schemas = [schema for schema in schemas if schema is not None]
|
|
95
|
+
return allowlist | collect_allowlist_from_schemas(
|
|
96
|
+
schemas=schemas,
|
|
97
|
+
channels=channels,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def collect_allowlist_from_schemas(
|
|
102
|
+
*,
|
|
103
|
+
schemas: list[type[Any]] | None = None,
|
|
104
|
+
channels: dict[str, Any] | None = None,
|
|
105
|
+
) -> set[tuple[str, ...]]:
|
|
106
|
+
allowlist: set[tuple[str, ...]] = set()
|
|
107
|
+
seen: set[Any] = set()
|
|
108
|
+
seen_ids: set[int] = set()
|
|
109
|
+
|
|
110
|
+
if schemas:
|
|
111
|
+
for schema in schemas:
|
|
112
|
+
_collect_from_type(schema, allowlist, seen, seen_ids)
|
|
113
|
+
|
|
114
|
+
if channels:
|
|
115
|
+
for channel in channels.values():
|
|
116
|
+
value_type = getattr(channel, "ValueType", None)
|
|
117
|
+
if value_type is not None:
|
|
118
|
+
_collect_from_type(value_type, allowlist, seen, seen_ids)
|
|
119
|
+
update_type = getattr(channel, "UpdateType", None)
|
|
120
|
+
if update_type is not None:
|
|
121
|
+
_collect_from_type(update_type, allowlist, seen, seen_ids)
|
|
122
|
+
|
|
123
|
+
return allowlist
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _collect_from_type(
|
|
127
|
+
typ: Any,
|
|
128
|
+
allowlist: set[tuple[str, ...]],
|
|
129
|
+
seen: set[Any],
|
|
130
|
+
seen_ids: set[int],
|
|
131
|
+
) -> None:
|
|
132
|
+
if _already_seen(typ, seen, seen_ids):
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if typ is Any or typ is None:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if typ is Literal:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if isinstance(typ, types.UnionType):
|
|
142
|
+
for arg in typ.__args__:
|
|
143
|
+
_collect_from_type(arg, allowlist, seen, seen_ids)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
origin = get_origin(typ)
|
|
147
|
+
if origin is Union:
|
|
148
|
+
for arg in get_args(typ):
|
|
149
|
+
_collect_from_type(arg, allowlist, seen, seen_ids)
|
|
150
|
+
return
|
|
151
|
+
if origin is Annotated or origin in (Required, NotRequired):
|
|
152
|
+
args = get_args(typ)
|
|
153
|
+
if args:
|
|
154
|
+
_collect_from_type(args[0], allowlist, seen, seen_ids)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
if origin is Literal:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if origin in (list, set, tuple, dict, deque, frozenset):
|
|
161
|
+
for arg in get_args(typ):
|
|
162
|
+
_collect_from_type(arg, allowlist, seen, seen_ids)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if hasattr(typ, "__supertype__"):
|
|
166
|
+
_collect_from_type(typ.__supertype__, allowlist, seen, seen_ids)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if is_typeddict(typ):
|
|
170
|
+
for field_type in _safe_get_type_hints(typ).values():
|
|
171
|
+
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if _is_pydantic_model(typ):
|
|
175
|
+
allowlist.add((typ.__module__, typ.__name__))
|
|
176
|
+
field_types = _safe_get_type_hints(typ)
|
|
177
|
+
if field_types:
|
|
178
|
+
for field_type in field_types.values():
|
|
179
|
+
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
|
180
|
+
else:
|
|
181
|
+
for field_type in _pydantic_field_types(typ):
|
|
182
|
+
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if dataclasses.is_dataclass(typ):
|
|
186
|
+
if typ_name := getattr(typ, "__name__", None):
|
|
187
|
+
allowlist.add((typ.__module__, typ_name))
|
|
188
|
+
field_types = _safe_get_type_hints(typ)
|
|
189
|
+
if field_types:
|
|
190
|
+
for field_type in field_types.values():
|
|
191
|
+
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
|
192
|
+
else:
|
|
193
|
+
for field in dataclasses.fields(typ):
|
|
194
|
+
_collect_from_type(field.type, allowlist, seen, seen_ids)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
if isinstance(typ, type) and issubclass(typ, Enum):
|
|
198
|
+
allowlist.add((typ.__module__, typ.__name__))
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _already_seen(typ: Any, seen: set[Any], seen_ids: set[int]) -> bool:
|
|
203
|
+
try:
|
|
204
|
+
if typ in seen:
|
|
205
|
+
return True
|
|
206
|
+
seen.add(typ)
|
|
207
|
+
return False
|
|
208
|
+
except TypeError:
|
|
209
|
+
typ_id = id(typ)
|
|
210
|
+
if typ_id in seen_ids:
|
|
211
|
+
return True
|
|
212
|
+
seen_ids.add(typ_id)
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _safe_get_type_hints(typ: Any) -> dict[str, Any]:
|
|
217
|
+
try:
|
|
218
|
+
module = sys.modules.get(getattr(typ, "__module__", ""))
|
|
219
|
+
globalns = module.__dict__ if module else None
|
|
220
|
+
localns = dict(vars(typ)) if hasattr(typ, "__dict__") else None
|
|
221
|
+
return get_type_hints(
|
|
222
|
+
typ, globalns=globalns, localns=localns, include_extras=True
|
|
223
|
+
)
|
|
224
|
+
except Exception:
|
|
225
|
+
return {}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _is_pydantic_model(typ: Any) -> bool:
|
|
229
|
+
if not isinstance(typ, type):
|
|
230
|
+
return False
|
|
231
|
+
if issubclass(typ, BaseModel):
|
|
232
|
+
return True
|
|
233
|
+
try:
|
|
234
|
+
from pydantic.v1 import BaseModel as BaseModelV1
|
|
235
|
+
except Exception:
|
|
236
|
+
return False
|
|
237
|
+
return issubclass(typ, BaseModelV1)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _pydantic_field_types(typ: type[Any]) -> list[Any]:
|
|
241
|
+
if hasattr(typ, "model_fields"):
|
|
242
|
+
return [
|
|
243
|
+
field.annotation
|
|
244
|
+
for field in typ.model_fields.values()
|
|
245
|
+
if getattr(field, "annotation", None) is not None
|
|
246
|
+
]
|
|
247
|
+
if hasattr(typ, "__fields__"):
|
|
248
|
+
return [
|
|
249
|
+
field.outer_type_
|
|
250
|
+
for field in typ.__fields__.values()
|
|
251
|
+
if getattr(field, "outer_type_", None) is not None
|
|
252
|
+
]
|
|
253
|
+
return []
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from coco_graph.types import TimeoutPolicy
|
|
7
|
+
|
|
8
|
+
_SYNC_TIMEOUT_PREFIX = (
|
|
9
|
+
"Node timeouts are only supported for async nodes because sync Python "
|
|
10
|
+
"execution cannot be safely cancelled in-process."
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def coerce_timeout_policy(
|
|
15
|
+
value: float | timedelta | TimeoutPolicy | None,
|
|
16
|
+
) -> TimeoutPolicy | None:
|
|
17
|
+
"""Normalize a timeout value to positive-second policy fields."""
|
|
18
|
+
return TimeoutPolicy.coerce(value)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sync_timeout_unsupported(
|
|
22
|
+
name: str, *, kind: Literal["Node", "Task"] = "Node"
|
|
23
|
+
) -> ValueError:
|
|
24
|
+
"""Build the canonical error for using `timeout` with a sync target."""
|
|
25
|
+
return ValueError(f"{_SYNC_TIMEOUT_PREFIX} {kind} {name!r} is sync.")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Private typing utilities for CocoGraph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import Field
|
|
6
|
+
from typing import Any, ClassVar, Protocol, TypeAlias
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from typing_extensions import TypedDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TypedDictLikeV1(Protocol):
|
|
13
|
+
"""Protocol to represent types that behave like TypedDicts
|
|
14
|
+
|
|
15
|
+
Version 1: using `ClassVar` for keys."""
|
|
16
|
+
|
|
17
|
+
__required_keys__: ClassVar[frozenset[str]]
|
|
18
|
+
__optional_keys__: ClassVar[frozenset[str]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TypedDictLikeV2(Protocol):
|
|
22
|
+
"""Protocol to represent types that behave like TypedDicts
|
|
23
|
+
|
|
24
|
+
Version 2: not using `ClassVar` for keys."""
|
|
25
|
+
|
|
26
|
+
__required_keys__: frozenset[str]
|
|
27
|
+
__optional_keys__: frozenset[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DataclassLike(Protocol):
|
|
31
|
+
"""Protocol to represent types that behave like dataclasses.
|
|
32
|
+
|
|
33
|
+
Inspired by the private _DataclassT from dataclasses that uses a similar protocol as a bound."""
|
|
34
|
+
|
|
35
|
+
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
StateLike: TypeAlias = TypedDictLikeV1 | TypedDictLikeV2 | DataclassLike | BaseModel
|
|
39
|
+
"""Type alias for state-like types.
|
|
40
|
+
|
|
41
|
+
It can either be a `TypedDict`, `dataclass`, or Pydantic `BaseModel`.
|
|
42
|
+
Note: we cannot use either `TypedDict` or `dataclass` directly due to limitations in type checking.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
MISSING = object()
|
|
46
|
+
"""Unset sentinel value."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DeprecatedKwargs(TypedDict):
|
|
50
|
+
"""TypedDict to use for extra keyword arguments, enabling type checking warnings for deprecated arguments."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
EMPTY_SEQ: tuple[str, ...] = tuple()
|
|
54
|
+
"""An empty sequence of strings."""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from coco_graph.checkpoint.serde.base import SerializerProtocol
|
|
8
|
+
from coco_graph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
9
|
+
|
|
10
|
+
ValueT = TypeVar("ValueT")
|
|
11
|
+
Namespace = tuple[str, ...]
|
|
12
|
+
FullKey = tuple[Namespace, str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseCache(ABC, Generic[ValueT]):
|
|
16
|
+
"""Base class for a cache."""
|
|
17
|
+
|
|
18
|
+
serde: SerializerProtocol = JsonPlusSerializer(pickle_fallback=False)
|
|
19
|
+
|
|
20
|
+
def __init__(self, *, serde: SerializerProtocol | None = None) -> None:
|
|
21
|
+
"""Initialize the cache with a serializer."""
|
|
22
|
+
self.serde = serde or self.serde
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
|
26
|
+
"""Get the cached values for the given keys."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
|
30
|
+
"""Asynchronously get the cached values for the given keys."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def set(self, pairs: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
|
34
|
+
"""Set the cached values for the given keys and TTLs."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def aset(self, pairs: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
|
38
|
+
"""Asynchronously set the cached values for the given keys and TTLs."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
|
42
|
+
"""Delete the cached values for the given namespaces.
|
|
43
|
+
If no namespaces are provided, clear all cached values."""
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
|
47
|
+
"""Asynchronously delete the cached values for the given namespaces.
|
|
48
|
+
If no namespaces are provided, clear all cached values."""
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
from coco_graph.cache.base import BaseCache, FullKey, Namespace, ValueT
|
|
8
|
+
from coco_graph.checkpoint.serde.base import SerializerProtocol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InMemoryCache(BaseCache[ValueT]):
|
|
12
|
+
def __init__(self, *, serde: SerializerProtocol | None = None):
|
|
13
|
+
super().__init__(serde=serde)
|
|
14
|
+
self._cache: dict[Namespace, dict[str, tuple[str, bytes, float | None]]] = {}
|
|
15
|
+
self._lock = threading.RLock()
|
|
16
|
+
|
|
17
|
+
def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
|
18
|
+
"""Get the cached values for the given keys."""
|
|
19
|
+
with self._lock:
|
|
20
|
+
if not keys:
|
|
21
|
+
return {}
|
|
22
|
+
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
|
23
|
+
values: dict[FullKey, ValueT] = {}
|
|
24
|
+
for ns_tuple, key in keys:
|
|
25
|
+
ns = Namespace(ns_tuple)
|
|
26
|
+
if ns in self._cache and key in self._cache[ns]:
|
|
27
|
+
enc, val, expiry = self._cache[ns][key]
|
|
28
|
+
if expiry is None or now < expiry:
|
|
29
|
+
values[(ns, key)] = self.serde.loads_typed((enc, val))
|
|
30
|
+
else:
|
|
31
|
+
del self._cache[ns][key]
|
|
32
|
+
return values
|
|
33
|
+
|
|
34
|
+
async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
|
35
|
+
"""Asynchronously get the cached values for the given keys."""
|
|
36
|
+
return self.get(keys)
|
|
37
|
+
|
|
38
|
+
def set(self, keys: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
|
39
|
+
"""Set the cached values for the given keys."""
|
|
40
|
+
with self._lock:
|
|
41
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
42
|
+
for (ns, key), (value, ttl) in keys.items():
|
|
43
|
+
if ttl is not None:
|
|
44
|
+
delta = datetime.timedelta(seconds=ttl)
|
|
45
|
+
expiry: float | None = (now + delta).timestamp()
|
|
46
|
+
else:
|
|
47
|
+
expiry = None
|
|
48
|
+
if ns not in self._cache:
|
|
49
|
+
self._cache[ns] = {}
|
|
50
|
+
self._cache[ns][key] = (
|
|
51
|
+
*self.serde.dumps_typed(value),
|
|
52
|
+
expiry,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def aset(self, keys: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
|
56
|
+
"""Asynchronously set the cached values for the given keys."""
|
|
57
|
+
self.set(keys)
|
|
58
|
+
|
|
59
|
+
def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
|
60
|
+
"""Delete the cached values for the given namespaces.
|
|
61
|
+
If no namespaces are provided, clear all cached values."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
if namespaces is None:
|
|
64
|
+
self._cache.clear()
|
|
65
|
+
else:
|
|
66
|
+
for ns in namespaces:
|
|
67
|
+
if ns in self._cache:
|
|
68
|
+
del self._cache[ns]
|
|
69
|
+
|
|
70
|
+
async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
|
71
|
+
"""Asynchronously delete the cached values for the given namespaces.
|
|
72
|
+
If no namespaces are provided, clear all cached values."""
|
|
73
|
+
self.clear(namespaces)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from coco_graph.cache.base import BaseCache, FullKey, Namespace, ValueT
|
|
7
|
+
from coco_graph.checkpoint.serde.base import SerializerProtocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RedisCache(BaseCache[ValueT]):
|
|
11
|
+
"""Redis-based cache implementation with TTL support."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
redis: Any,
|
|
16
|
+
*,
|
|
17
|
+
serde: SerializerProtocol | None = None,
|
|
18
|
+
prefix: str = "coco_graph:cache:",
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Initialize the cache with a Redis client.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
redis: Redis client instance (sync or async)
|
|
24
|
+
serde: Serializer to use for values
|
|
25
|
+
prefix: Key prefix for all cached values
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(serde=serde)
|
|
28
|
+
self.redis = redis
|
|
29
|
+
self.prefix = prefix
|
|
30
|
+
|
|
31
|
+
def _make_key(self, ns: Namespace, key: str) -> str:
|
|
32
|
+
"""Create a Redis key from namespace and key."""
|
|
33
|
+
ns_str = ":".join(ns) if ns else ""
|
|
34
|
+
return f"{self.prefix}{ns_str}:{key}" if ns_str else f"{self.prefix}{key}"
|
|
35
|
+
|
|
36
|
+
def _parse_key(self, redis_key: str) -> tuple[Namespace, str]:
|
|
37
|
+
"""Parse a Redis key back to namespace and key."""
|
|
38
|
+
if not redis_key.startswith(self.prefix):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Key {redis_key} does not start with prefix {self.prefix}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
remaining = redis_key[len(self.prefix) :]
|
|
44
|
+
if ":" in remaining:
|
|
45
|
+
parts = remaining.split(":")
|
|
46
|
+
key = parts[-1]
|
|
47
|
+
ns_parts = parts[:-1]
|
|
48
|
+
return (tuple(ns_parts), key)
|
|
49
|
+
else:
|
|
50
|
+
return (tuple(), remaining)
|
|
51
|
+
|
|
52
|
+
def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
|
53
|
+
"""Get the cached values for the given keys."""
|
|
54
|
+
if not keys:
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
# Build Redis keys
|
|
58
|
+
redis_keys = [self._make_key(ns, key) for ns, key in keys]
|
|
59
|
+
|
|
60
|
+
# Get values from Redis using MGET
|
|
61
|
+
try:
|
|
62
|
+
raw_values = self.redis.mget(redis_keys)
|
|
63
|
+
except Exception:
|
|
64
|
+
# If Redis is unavailable, return empty dict
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
values: dict[FullKey, ValueT] = {}
|
|
68
|
+
for i, raw_value in enumerate(raw_values):
|
|
69
|
+
if raw_value is not None:
|
|
70
|
+
try:
|
|
71
|
+
# Deserialize the value
|
|
72
|
+
encoding, data = raw_value.split(b":", 1)
|
|
73
|
+
values[keys[i]] = self.serde.loads_typed((encoding.decode(), data))
|
|
74
|
+
except Exception:
|
|
75
|
+
# Skip corrupted entries
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
return values
|
|
79
|
+
|
|
80
|
+
async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
|
81
|
+
"""Asynchronously get the cached values for the given keys."""
|
|
82
|
+
return self.get(keys)
|
|
83
|
+
|
|
84
|
+
def set(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
|
85
|
+
"""Set the cached values for the given keys and TTLs."""
|
|
86
|
+
if not mapping:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Use pipeline for efficient batch operations
|
|
90
|
+
pipe = self.redis.pipeline()
|
|
91
|
+
|
|
92
|
+
for (ns, key), (value, ttl) in mapping.items():
|
|
93
|
+
redis_key = self._make_key(ns, key)
|
|
94
|
+
encoding, data = self.serde.dumps_typed(value)
|
|
95
|
+
|
|
96
|
+
# Store as "encoding:data" format
|
|
97
|
+
serialized_value = f"{encoding}:".encode() + data
|
|
98
|
+
|
|
99
|
+
if ttl is not None:
|
|
100
|
+
pipe.setex(redis_key, ttl, serialized_value)
|
|
101
|
+
else:
|
|
102
|
+
pipe.set(redis_key, serialized_value)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
pipe.execute()
|
|
106
|
+
except Exception:
|
|
107
|
+
# Silently fail if Redis is unavailable
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
async def aset(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
|
111
|
+
"""Asynchronously set the cached values for the given keys and TTLs."""
|
|
112
|
+
self.set(mapping)
|
|
113
|
+
|
|
114
|
+
def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
|
115
|
+
"""Delete the cached values for the given namespaces.
|
|
116
|
+
If no namespaces are provided, clear all cached values."""
|
|
117
|
+
try:
|
|
118
|
+
if namespaces is None:
|
|
119
|
+
# Clear all keys with our prefix
|
|
120
|
+
pattern = f"{self.prefix}*"
|
|
121
|
+
keys = self.redis.keys(pattern)
|
|
122
|
+
if keys:
|
|
123
|
+
self.redis.delete(*keys)
|
|
124
|
+
else:
|
|
125
|
+
# Clear specific namespaces
|
|
126
|
+
keys_to_delete = []
|
|
127
|
+
for ns in namespaces:
|
|
128
|
+
ns_str = ":".join(ns) if ns else ""
|
|
129
|
+
pattern = (
|
|
130
|
+
f"{self.prefix}{ns_str}:*" if ns_str else f"{self.prefix}*"
|
|
131
|
+
)
|
|
132
|
+
keys = self.redis.keys(pattern)
|
|
133
|
+
keys_to_delete.extend(keys)
|
|
134
|
+
|
|
135
|
+
if keys_to_delete:
|
|
136
|
+
self.redis.delete(*keys_to_delete)
|
|
137
|
+
except Exception:
|
|
138
|
+
# Silently fail if Redis is unavailable
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
|
142
|
+
"""Asynchronously delete the cached values for the given namespaces.
|
|
143
|
+
If no namespaces are provided, clear all cached values."""
|
|
144
|
+
self.clear(namespaces)
|