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,298 @@
|
|
|
1
|
+
"""Utility to convert a user provided function into a Runnable with a ChannelWrite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
import sys
|
|
9
|
+
import types
|
|
10
|
+
from collections.abc import Awaitable, Callable, Generator, Sequence
|
|
11
|
+
from datetime import timedelta
|
|
12
|
+
from typing import Any, Generic, TypeVar, cast
|
|
13
|
+
|
|
14
|
+
from coco_core.runnables import Runnable
|
|
15
|
+
from typing_extensions import ParamSpec
|
|
16
|
+
|
|
17
|
+
from coco_graph._internal._constants import CONF, CONFIG_KEY_CALL, RETURN
|
|
18
|
+
from coco_graph._internal._runnable import (
|
|
19
|
+
RunnableCallable,
|
|
20
|
+
RunnableSeq,
|
|
21
|
+
is_async_callable,
|
|
22
|
+
run_in_executor,
|
|
23
|
+
)
|
|
24
|
+
from coco_graph._internal._timeout import (
|
|
25
|
+
coerce_timeout_policy,
|
|
26
|
+
sync_timeout_unsupported,
|
|
27
|
+
)
|
|
28
|
+
from coco_graph.config import get_config
|
|
29
|
+
from coco_graph.pregel._write import ChannelWrite, ChannelWriteEntry
|
|
30
|
+
from coco_graph.types import CachePolicy, RetryPolicy, TimeoutPolicy
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Utilities borrowed from cloudpickle.
|
|
34
|
+
# https://github.com/cloudpipe/cloudpickle/blob/6220b0ce83ffee5e47e06770a1ee38ca9e47c850/cloudpickle/cloudpickle.py#L265
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _getattribute(obj: Any, name: str) -> Any:
|
|
38
|
+
parent = None
|
|
39
|
+
for subpath in name.split("."):
|
|
40
|
+
if subpath == "<locals>":
|
|
41
|
+
raise AttributeError(f"Can't get local attribute {name!r} on {obj!r}")
|
|
42
|
+
try:
|
|
43
|
+
parent = obj
|
|
44
|
+
obj = getattr(obj, subpath)
|
|
45
|
+
except AttributeError:
|
|
46
|
+
raise AttributeError(f"Can't get attribute {name!r} on {obj!r}") from None
|
|
47
|
+
return obj, parent
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _whichmodule(obj: Any, name: str) -> str | None:
|
|
51
|
+
"""Find the module an object belongs to.
|
|
52
|
+
|
|
53
|
+
This function differs from ``pickle.whichmodule`` in two ways:
|
|
54
|
+
- it does not mangle the cases where obj's module is __main__ and obj was
|
|
55
|
+
not found in any module.
|
|
56
|
+
- Errors arising during module introspection are ignored, as those errors
|
|
57
|
+
are considered unwanted side effects.
|
|
58
|
+
"""
|
|
59
|
+
module_name = getattr(obj, "__module__", None)
|
|
60
|
+
|
|
61
|
+
if module_name is not None:
|
|
62
|
+
return module_name
|
|
63
|
+
# Protect the iteration by using a copy of sys.modules against dynamic
|
|
64
|
+
# modules that trigger imports of other modules upon calls to getattr or
|
|
65
|
+
# other threads importing at the same time.
|
|
66
|
+
for module_name, module in sys.modules.copy().items():
|
|
67
|
+
# Some modules such as coverage can inject non-module objects inside
|
|
68
|
+
# sys.modules
|
|
69
|
+
if (
|
|
70
|
+
module_name == "__main__"
|
|
71
|
+
or module_name == "__mp_main__"
|
|
72
|
+
or module is None
|
|
73
|
+
or not isinstance(module, types.ModuleType)
|
|
74
|
+
):
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
if _getattribute(module, name)[0] is obj:
|
|
78
|
+
return module_name
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def identifier(obj: Any, name: str | None = None) -> str | None:
|
|
85
|
+
"""Return the module and name of an object."""
|
|
86
|
+
from coco_graph._internal._runnable import RunnableCallable, RunnableSeq
|
|
87
|
+
from coco_graph.pregel._read import PregelNode
|
|
88
|
+
|
|
89
|
+
if isinstance(obj, PregelNode):
|
|
90
|
+
obj = obj.bound
|
|
91
|
+
if isinstance(obj, RunnableSeq):
|
|
92
|
+
obj = obj.steps[0]
|
|
93
|
+
if isinstance(obj, RunnableCallable):
|
|
94
|
+
obj = obj.func
|
|
95
|
+
if name is None:
|
|
96
|
+
name = getattr(obj, "__qualname__", None)
|
|
97
|
+
if name is None: # pragma: no cover
|
|
98
|
+
# This used to be needed for Python 2.7 support but is probably not
|
|
99
|
+
# needed anymore. However we keep the __name__ introspection in case
|
|
100
|
+
# users of cloudpickle rely on this old behavior for unknown reasons.
|
|
101
|
+
name = getattr(obj, "__name__", None)
|
|
102
|
+
if name is None:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
module_name = getattr(obj, "__module__", None)
|
|
106
|
+
if module_name is None:
|
|
107
|
+
# In this case, obj.__module__ is None. obj is thus treated as dynamic.
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
return f"{module_name}.{name}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _lookup_module_and_qualname(
|
|
114
|
+
obj: Any, name: str | None = None
|
|
115
|
+
) -> tuple[types.ModuleType, str] | None:
|
|
116
|
+
if name is None:
|
|
117
|
+
name = getattr(obj, "__qualname__", None)
|
|
118
|
+
if name is None: # pragma: no cover
|
|
119
|
+
# This used to be needed for Python 2.7 support but is probably not
|
|
120
|
+
# needed anymore. However we keep the __name__ introspection in case
|
|
121
|
+
# users of cloudpickle rely on this old behavior for unknown reasons.
|
|
122
|
+
name = getattr(obj, "__name__", None)
|
|
123
|
+
if name is None:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
module_name = _whichmodule(obj, name)
|
|
127
|
+
|
|
128
|
+
if module_name is None:
|
|
129
|
+
# In this case, obj.__module__ is None AND obj was not found in any
|
|
130
|
+
# imported module. obj is thus treated as dynamic.
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
if module_name == "__main__":
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
# Note: if module_name is in sys.modules, the corresponding module is
|
|
137
|
+
# assumed importable at unpickling time. See #357
|
|
138
|
+
module = sys.modules.get(module_name, None)
|
|
139
|
+
if module is None:
|
|
140
|
+
# The main reason why obj's module would not be imported is that this
|
|
141
|
+
# module has been dynamically created, using for example
|
|
142
|
+
# types.ModuleType. The other possibility is that module was removed
|
|
143
|
+
# from sys.modules after obj was created/imported. But this case is not
|
|
144
|
+
# supported, as the standard pickle does not support it either.
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
obj2, parent = _getattribute(module, name)
|
|
149
|
+
except AttributeError:
|
|
150
|
+
# obj was not found inside the module it points to
|
|
151
|
+
return None
|
|
152
|
+
if obj2 is not obj:
|
|
153
|
+
return None
|
|
154
|
+
return module, name
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _explode_args_trace_inputs(
|
|
158
|
+
sig: inspect.Signature, input: tuple[tuple[Any, ...], dict[str, Any]]
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
args, kwargs = input
|
|
161
|
+
bound = sig.bind_partial(*args, **kwargs)
|
|
162
|
+
bound.apply_defaults()
|
|
163
|
+
arguments = dict(bound.arguments)
|
|
164
|
+
arguments.pop("self", None)
|
|
165
|
+
arguments.pop("cls", None)
|
|
166
|
+
for param_name, param in sig.parameters.items():
|
|
167
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
168
|
+
# Update with the **kwargs, and remove the original entry
|
|
169
|
+
# This is to help flatten out keyword arguments
|
|
170
|
+
if param_name in arguments:
|
|
171
|
+
arguments.update(arguments.pop(param_name))
|
|
172
|
+
return arguments
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_runnable_for_entrypoint(func: Callable[..., Any]) -> Runnable:
|
|
176
|
+
key = (func, False)
|
|
177
|
+
if key in CACHE:
|
|
178
|
+
return CACHE[key]
|
|
179
|
+
else:
|
|
180
|
+
if is_async_callable(func):
|
|
181
|
+
run = RunnableCallable(
|
|
182
|
+
None, func, name=func.__name__, trace=False, recurse=False
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
afunc = functools.update_wrapper(
|
|
186
|
+
functools.partial(run_in_executor, None, func), func
|
|
187
|
+
)
|
|
188
|
+
run = RunnableCallable(
|
|
189
|
+
func,
|
|
190
|
+
afunc,
|
|
191
|
+
name=func.__name__,
|
|
192
|
+
trace=False,
|
|
193
|
+
recurse=False,
|
|
194
|
+
)
|
|
195
|
+
if not _lookup_module_and_qualname(func):
|
|
196
|
+
return run
|
|
197
|
+
return CACHE.setdefault(key, run)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_runnable_for_task(func: Callable[..., Any]) -> Runnable:
|
|
201
|
+
key = (func, True)
|
|
202
|
+
if key in CACHE:
|
|
203
|
+
return CACHE[key]
|
|
204
|
+
else:
|
|
205
|
+
if hasattr(func, "__name__"):
|
|
206
|
+
name = func.__name__
|
|
207
|
+
elif hasattr(func, "func"):
|
|
208
|
+
name = func.func.__name__
|
|
209
|
+
elif hasattr(func, "__class__"):
|
|
210
|
+
name = func.__class__.__name__
|
|
211
|
+
else:
|
|
212
|
+
name = str(func)
|
|
213
|
+
|
|
214
|
+
if is_async_callable(func):
|
|
215
|
+
run = RunnableCallable(
|
|
216
|
+
None,
|
|
217
|
+
func,
|
|
218
|
+
explode_args=True,
|
|
219
|
+
name=name,
|
|
220
|
+
trace=False,
|
|
221
|
+
recurse=False,
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
run = RunnableCallable(
|
|
225
|
+
func,
|
|
226
|
+
functools.wraps(func)(functools.partial(run_in_executor, None, func)),
|
|
227
|
+
explode_args=True,
|
|
228
|
+
name=name,
|
|
229
|
+
trace=False,
|
|
230
|
+
recurse=False,
|
|
231
|
+
)
|
|
232
|
+
seq = RunnableSeq(
|
|
233
|
+
run,
|
|
234
|
+
ChannelWrite([ChannelWriteEntry(RETURN)]),
|
|
235
|
+
name=name,
|
|
236
|
+
trace_inputs=functools.partial(
|
|
237
|
+
_explode_args_trace_inputs, inspect.signature(func)
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
if not _lookup_module_and_qualname(func):
|
|
241
|
+
return seq
|
|
242
|
+
return CACHE.setdefault(key, seq)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
CACHE: dict[tuple[Callable[..., Any], bool], Runnable] = {}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
P = ParamSpec("P")
|
|
249
|
+
P1 = TypeVar("P1")
|
|
250
|
+
T = TypeVar("T")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class SyncAsyncFuture(Generic[T], concurrent.futures.Future[T]):
|
|
254
|
+
def __await__(self) -> Generator[T, None, T]:
|
|
255
|
+
yield cast(T, ...)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def call(
|
|
259
|
+
func: Callable[P, Awaitable[T]] | Callable[P, T],
|
|
260
|
+
*args: Any,
|
|
261
|
+
retry_policy: Sequence[RetryPolicy] | None = None,
|
|
262
|
+
cache_policy: CachePolicy | None = None,
|
|
263
|
+
timeout: float | timedelta | TimeoutPolicy | None = None,
|
|
264
|
+
**kwargs: Any,
|
|
265
|
+
) -> SyncAsyncFuture[T]:
|
|
266
|
+
return _call_with_options(
|
|
267
|
+
func,
|
|
268
|
+
args,
|
|
269
|
+
kwargs,
|
|
270
|
+
retry_policy=retry_policy,
|
|
271
|
+
cache_policy=cache_policy,
|
|
272
|
+
timeout=coerce_timeout_policy(timeout),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _call_with_options(
|
|
277
|
+
func: Callable[P, Awaitable[T]] | Callable[P, T],
|
|
278
|
+
args: tuple[Any, ...],
|
|
279
|
+
kwargs: dict[str, Any],
|
|
280
|
+
*,
|
|
281
|
+
retry_policy: Sequence[RetryPolicy] | None = None,
|
|
282
|
+
cache_policy: CachePolicy | None = None,
|
|
283
|
+
timeout: TimeoutPolicy | None = None,
|
|
284
|
+
) -> SyncAsyncFuture[T]:
|
|
285
|
+
if timeout is not None and not is_async_callable(func):
|
|
286
|
+
name = getattr(func, "__name__", func.__class__.__name__)
|
|
287
|
+
raise sync_timeout_unsupported(name, kind="Task")
|
|
288
|
+
config = get_config()
|
|
289
|
+
impl = config[CONF][CONFIG_KEY_CALL]
|
|
290
|
+
fut = impl(
|
|
291
|
+
func,
|
|
292
|
+
(args, kwargs),
|
|
293
|
+
retry_policy=retry_policy,
|
|
294
|
+
cache_policy=cache_policy,
|
|
295
|
+
callbacks=config["callbacks"],
|
|
296
|
+
timeout=timeout,
|
|
297
|
+
)
|
|
298
|
+
return fut
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from coco_core.runnables import RunnableConfig
|
|
9
|
+
from coco_graph.checkpoint.base import (
|
|
10
|
+
BaseCheckpointSaver,
|
|
11
|
+
Checkpoint,
|
|
12
|
+
)
|
|
13
|
+
from coco_graph.checkpoint.base.id import uuid6
|
|
14
|
+
from coco_graph.checkpoint.serde.types import _DeltaSnapshot
|
|
15
|
+
|
|
16
|
+
from coco_graph._internal._config import DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
|
|
17
|
+
from coco_graph._internal._constants import PUSH
|
|
18
|
+
from coco_graph._internal._typing import MISSING
|
|
19
|
+
from coco_graph.channels.base import BaseChannel
|
|
20
|
+
from coco_graph.channels.delta import DeltaChannel
|
|
21
|
+
from coco_graph.managed.base import ManagedValueMapping, ManagedValueSpec
|
|
22
|
+
|
|
23
|
+
LATEST_VERSION = 4
|
|
24
|
+
|
|
25
|
+
GetNextVersion = Callable[[Any, None], Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def empty_checkpoint() -> Checkpoint:
|
|
29
|
+
return Checkpoint(
|
|
30
|
+
v=LATEST_VERSION,
|
|
31
|
+
id=str(uuid6(clock_seq=-2)),
|
|
32
|
+
ts=datetime.now(timezone.utc).isoformat(),
|
|
33
|
+
channel_values={},
|
|
34
|
+
channel_versions={},
|
|
35
|
+
versions_seen={},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def exit_delta_task_id(step: int, task_id: str) -> str:
|
|
40
|
+
"""Synthetic task id for exit-mode DeltaChannel writes.
|
|
41
|
+
|
|
42
|
+
Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
|
|
43
|
+
preserves chronological order while remaining a valid RFC UUID (required by
|
|
44
|
+
Postgres `checkpoint_writes.task_id uuid` columns).
|
|
45
|
+
"""
|
|
46
|
+
parts = str(uuid.UUID(task_id)).split("-")
|
|
47
|
+
return f"{step:08d}-{parts[1]}-{parts[2]}-{parts[3]}-{parts[4]}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def delta_channels_to_snapshot(
|
|
51
|
+
channels: Mapping[str, BaseChannel],
|
|
52
|
+
counters_since_delta_snapshot: Mapping[str, tuple[int, int]],
|
|
53
|
+
) -> set[str]:
|
|
54
|
+
"""Return the set of DeltaChannel names that should snapshot now.
|
|
55
|
+
|
|
56
|
+
A channel snapshots when EITHER its accumulated update count reaches
|
|
57
|
+
`snapshot_frequency` OR the total supersteps since its last snapshot
|
|
58
|
+
reaches `DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`. This is a pure
|
|
59
|
+
predicate — no mutation.
|
|
60
|
+
"""
|
|
61
|
+
result: set[str] = set()
|
|
62
|
+
for name, ch in channels.items():
|
|
63
|
+
if not isinstance(ch, DeltaChannel) or not ch.is_available():
|
|
64
|
+
continue
|
|
65
|
+
updates, supersteps = counters_since_delta_snapshot.get(name, (0, 0))
|
|
66
|
+
if (
|
|
67
|
+
updates >= ch.snapshot_frequency
|
|
68
|
+
or supersteps >= DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
|
|
69
|
+
):
|
|
70
|
+
result.add(name)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_updated_channels_from_tasks(
|
|
75
|
+
run_tasks: Iterable[Any],
|
|
76
|
+
) -> set[str]:
|
|
77
|
+
"""Channel names written by an update_state superstep (excluding PUSH)."""
|
|
78
|
+
return {c for task in run_tasks for c, _ in task.writes if c != PUSH}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_delta_channels_from_all_channels(
|
|
82
|
+
channels: Mapping[str, BaseChannel],
|
|
83
|
+
) -> set[str]:
|
|
84
|
+
"""DeltaChannels to snapshot on the first update_state of a fresh thread."""
|
|
85
|
+
return {
|
|
86
|
+
k
|
|
87
|
+
for k, ch in channels.items()
|
|
88
|
+
if isinstance(ch, DeltaChannel) and ch.is_available()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_metadata_for_update_state_api(
|
|
93
|
+
channels: Mapping[str, BaseChannel],
|
|
94
|
+
updated_channels: set[str],
|
|
95
|
+
*,
|
|
96
|
+
prev_metadata: Mapping[str, Any] | None,
|
|
97
|
+
) -> dict[str, tuple[int, int]]:
|
|
98
|
+
"""Advance ``counters_since_delta_snapshot`` for update_state on a non-fresh thread.
|
|
99
|
+
|
|
100
|
+
Mirrors the per-superstep counter bump in ``_loop._put_checkpoint``.
|
|
101
|
+
"""
|
|
102
|
+
prev_counters = dict(
|
|
103
|
+
(prev_metadata or {}).get("counters_since_delta_snapshot") or {}
|
|
104
|
+
)
|
|
105
|
+
new_counters: dict[str, tuple[int, int]] = {}
|
|
106
|
+
for ch_name, ch in channels.items():
|
|
107
|
+
if not isinstance(ch, DeltaChannel):
|
|
108
|
+
continue
|
|
109
|
+
u, s = prev_counters.get(ch_name, (0, 0))
|
|
110
|
+
s += 1
|
|
111
|
+
if ch_name in updated_channels:
|
|
112
|
+
u += 1
|
|
113
|
+
new_counters[ch_name] = (u, s)
|
|
114
|
+
return new_counters
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def create_checkpoint_plan_for_update_state_api(
|
|
118
|
+
channels: Mapping[str, BaseChannel],
|
|
119
|
+
updated_channels: set[str],
|
|
120
|
+
*,
|
|
121
|
+
step: int,
|
|
122
|
+
parents: dict[str, Any],
|
|
123
|
+
saved_metadata: Mapping[str, Any] | None,
|
|
124
|
+
is_fresh_thread: bool,
|
|
125
|
+
) -> tuple[set[str], dict[str, Any]]:
|
|
126
|
+
"""Return ``(channels_to_snapshot, metadata)`` for an update_state head."""
|
|
127
|
+
metadata: dict[str, Any] = {
|
|
128
|
+
"source": "update",
|
|
129
|
+
"step": step,
|
|
130
|
+
"parents": parents,
|
|
131
|
+
}
|
|
132
|
+
if is_fresh_thread:
|
|
133
|
+
return get_delta_channels_from_all_channels(channels), metadata
|
|
134
|
+
|
|
135
|
+
new_counters = create_metadata_for_update_state_api(
|
|
136
|
+
channels,
|
|
137
|
+
updated_channels,
|
|
138
|
+
prev_metadata=saved_metadata,
|
|
139
|
+
)
|
|
140
|
+
channels_to_snapshot = delta_channels_to_snapshot(channels, new_counters)
|
|
141
|
+
for k in channels_to_snapshot:
|
|
142
|
+
new_counters[k] = (0, 0)
|
|
143
|
+
non_zero = {k: v for k, v in new_counters.items() if v != (0, 0)}
|
|
144
|
+
if non_zero:
|
|
145
|
+
metadata["counters_since_delta_snapshot"] = non_zero
|
|
146
|
+
return channels_to_snapshot, metadata
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_checkpoint(
|
|
150
|
+
checkpoint: Checkpoint,
|
|
151
|
+
channels: Mapping[str, BaseChannel] | None,
|
|
152
|
+
step: int,
|
|
153
|
+
*,
|
|
154
|
+
id: str | None = None,
|
|
155
|
+
updated_channels: set[str] | None = None,
|
|
156
|
+
get_next_version: GetNextVersion | None = None,
|
|
157
|
+
channels_to_snapshot: set[str] | None = None,
|
|
158
|
+
) -> Checkpoint:
|
|
159
|
+
"""Build a new Checkpoint from the previous one and live channel state.
|
|
160
|
+
|
|
161
|
+
For each name in `channels_to_snapshot`, a `_DeltaSnapshot(value)` blob
|
|
162
|
+
is written into `channel_values[k]`. Other delta channels are omitted
|
|
163
|
+
from `channel_values` — the ancestor walk reconstructs their state
|
|
164
|
+
from `checkpoint_writes`. Callers compute the set via
|
|
165
|
+
`delta_channels_to_snapshot(channels, counters)`; defaults to empty
|
|
166
|
+
(no snapshots) when not provided.
|
|
167
|
+
"""
|
|
168
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
169
|
+
channels_to_snapshot = channels_to_snapshot or set()
|
|
170
|
+
if channels is None:
|
|
171
|
+
values = checkpoint["channel_values"]
|
|
172
|
+
channel_versions = checkpoint["channel_versions"]
|
|
173
|
+
else:
|
|
174
|
+
values = {}
|
|
175
|
+
channel_versions = dict(checkpoint["channel_versions"])
|
|
176
|
+
for k in channels:
|
|
177
|
+
if k not in channel_versions:
|
|
178
|
+
continue
|
|
179
|
+
ch = channels[k]
|
|
180
|
+
if k in channels_to_snapshot:
|
|
181
|
+
# Callers force a full snapshot blob here: exit mode when a
|
|
182
|
+
# delta channel reaches its snapshot cadence, and update_state
|
|
183
|
+
# on a fresh thread (no ancestor to replay writes from). The
|
|
184
|
+
# manual version-bump below only applies to the exit-mode case.
|
|
185
|
+
#
|
|
186
|
+
# In exit mode, the snapshot decision is deferred to exit
|
|
187
|
+
# time (intermediate steps have do_checkpoint=False). The
|
|
188
|
+
# channel's count may have reached snapshot_frequency over
|
|
189
|
+
# several supersteps, but the LAST superstep may not have
|
|
190
|
+
# written to this channel. In that case apply_writes()
|
|
191
|
+
# (in _algo.py) didn't bump this channel's version, so
|
|
192
|
+
# saver.put() wouldn't include it in new_versions and
|
|
193
|
+
# the snapshot blob would be silently dropped. The manual
|
|
194
|
+
# bump below closes the gap. In sync/async durability this
|
|
195
|
+
# branch is effectively dead code (the step that pushes
|
|
196
|
+
# the count to freq always writes the channel).
|
|
197
|
+
if get_next_version is not None and (
|
|
198
|
+
updated_channels is None or k not in updated_channels
|
|
199
|
+
):
|
|
200
|
+
channel_versions[k] = get_next_version(channel_versions[k], None)
|
|
201
|
+
values[k] = _DeltaSnapshot(ch.get())
|
|
202
|
+
else:
|
|
203
|
+
v = ch.checkpoint()
|
|
204
|
+
if v is not MISSING:
|
|
205
|
+
values[k] = v
|
|
206
|
+
return Checkpoint(
|
|
207
|
+
v=LATEST_VERSION,
|
|
208
|
+
ts=ts,
|
|
209
|
+
id=id or str(uuid6(clock_seq=step)),
|
|
210
|
+
channel_values=values,
|
|
211
|
+
channel_versions=channel_versions,
|
|
212
|
+
versions_seen=checkpoint["versions_seen"],
|
|
213
|
+
updated_channels=None if updated_channels is None else sorted(updated_channels),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _needs_replay(spec: BaseChannel, stored: object) -> bool:
|
|
218
|
+
"""True if `spec` is a `DeltaChannel` and no value is stored at this
|
|
219
|
+
checkpoint, requiring an ancestor walk to reconstruct.
|
|
220
|
+
|
|
221
|
+
`_DeltaSnapshot` blobs and plain values (migration) resolve directly via
|
|
222
|
+
`from_checkpoint` — only absence (`MISSING`) triggers replay.
|
|
223
|
+
"""
|
|
224
|
+
if not isinstance(spec, DeltaChannel):
|
|
225
|
+
return False
|
|
226
|
+
return stored is MISSING
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def channels_from_checkpoint(
|
|
230
|
+
specs: Mapping[str, BaseChannel | ManagedValueSpec],
|
|
231
|
+
checkpoint: Checkpoint,
|
|
232
|
+
*,
|
|
233
|
+
saver: BaseCheckpointSaver | None = None,
|
|
234
|
+
config: RunnableConfig | None = None,
|
|
235
|
+
) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
|
|
236
|
+
"""Hydrate channels from a checkpoint.
|
|
237
|
+
|
|
238
|
+
For most channels, `spec.from_checkpoint(checkpoint["channel_values"][k])`
|
|
239
|
+
is sufficient. `DeltaChannel` is the exception: when the channel is
|
|
240
|
+
absent from `channel_values`, an ancestor walk via
|
|
241
|
+
`saver.get_delta_channel_history` is required to find the nearest seed
|
|
242
|
+
(`_DeltaSnapshot` blob or pre-migration plain value) and accumulate
|
|
243
|
+
the writes between it and the target. All delta channels needing
|
|
244
|
+
replay are batched into a single saver call.
|
|
245
|
+
"""
|
|
246
|
+
channel_specs: dict[str, BaseChannel] = {}
|
|
247
|
+
managed_specs: dict[str, ManagedValueSpec] = {}
|
|
248
|
+
for k, v in specs.items():
|
|
249
|
+
if isinstance(v, BaseChannel):
|
|
250
|
+
channel_specs[k] = v
|
|
251
|
+
else:
|
|
252
|
+
managed_specs[k] = v
|
|
253
|
+
|
|
254
|
+
delta_channels: list[str] = [
|
|
255
|
+
k
|
|
256
|
+
for k, spec in channel_specs.items()
|
|
257
|
+
if _needs_replay(spec, checkpoint["channel_values"].get(k, MISSING))
|
|
258
|
+
]
|
|
259
|
+
histories: Mapping[str, Any] = {}
|
|
260
|
+
if delta_channels and saver is not None and config is not None:
|
|
261
|
+
histories = saver.get_delta_channel_history(
|
|
262
|
+
config=config, channels=delta_channels
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
channels: dict[str, BaseChannel] = {}
|
|
266
|
+
for k, spec in channel_specs.items():
|
|
267
|
+
ch: BaseChannel
|
|
268
|
+
if k in histories:
|
|
269
|
+
delta_spec = cast(DeltaChannel, spec)
|
|
270
|
+
history = histories[k]
|
|
271
|
+
replay_ch = delta_spec.from_checkpoint(history.get("seed", MISSING))
|
|
272
|
+
replay_ch.replay_writes(history["writes"])
|
|
273
|
+
ch = replay_ch
|
|
274
|
+
else:
|
|
275
|
+
ch = spec.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
|
276
|
+
channels[k] = ch
|
|
277
|
+
return channels, managed_specs
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def achannels_from_checkpoint(
|
|
281
|
+
specs: Mapping[str, BaseChannel | ManagedValueSpec],
|
|
282
|
+
checkpoint: Checkpoint,
|
|
283
|
+
*,
|
|
284
|
+
saver: BaseCheckpointSaver | None = None,
|
|
285
|
+
config: RunnableConfig | None = None,
|
|
286
|
+
) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
|
|
287
|
+
"""Async version of `channels_from_checkpoint`. See docstring there."""
|
|
288
|
+
channel_specs: dict[str, BaseChannel] = {}
|
|
289
|
+
managed_specs: dict[str, ManagedValueSpec] = {}
|
|
290
|
+
for k, v in specs.items():
|
|
291
|
+
if isinstance(v, BaseChannel):
|
|
292
|
+
channel_specs[k] = v
|
|
293
|
+
else:
|
|
294
|
+
managed_specs[k] = v
|
|
295
|
+
|
|
296
|
+
delta_channels: list[str] = [
|
|
297
|
+
k
|
|
298
|
+
for k, spec in channel_specs.items()
|
|
299
|
+
if _needs_replay(spec, checkpoint["channel_values"].get(k, MISSING))
|
|
300
|
+
]
|
|
301
|
+
histories: Mapping[str, Any] = {}
|
|
302
|
+
if delta_channels and saver is not None and config is not None:
|
|
303
|
+
histories = await saver.aget_delta_channel_history(
|
|
304
|
+
config=config, channels=delta_channels
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
channels: dict[str, BaseChannel] = {}
|
|
308
|
+
for k, spec in channel_specs.items():
|
|
309
|
+
ch: BaseChannel
|
|
310
|
+
if k in histories:
|
|
311
|
+
delta_spec = cast(DeltaChannel, spec)
|
|
312
|
+
history = histories[k]
|
|
313
|
+
replay_ch = delta_spec.from_checkpoint(history.get("seed", MISSING))
|
|
314
|
+
replay_ch.replay_writes(history["writes"])
|
|
315
|
+
ch = replay_ch
|
|
316
|
+
else:
|
|
317
|
+
ch = spec.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
|
318
|
+
channels[k] = ch
|
|
319
|
+
return channels, managed_specs
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
|
323
|
+
return Checkpoint(
|
|
324
|
+
v=checkpoint["v"],
|
|
325
|
+
ts=checkpoint["ts"],
|
|
326
|
+
id=checkpoint["id"],
|
|
327
|
+
channel_values=checkpoint["channel_values"].copy(),
|
|
328
|
+
channel_versions=checkpoint["channel_versions"].copy(),
|
|
329
|
+
versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
|
|
330
|
+
updated_channels=checkpoint.get("updated_channels", None),
|
|
331
|
+
)
|
|
File without changes
|