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,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import types
|
|
5
|
+
import weakref
|
|
6
|
+
from collections.abc import Generator, Sequence
|
|
7
|
+
from typing import Annotated, Any, Optional, Union, get_origin, get_type_hints
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from typing_extensions import NotRequired, ReadOnly, Required
|
|
11
|
+
|
|
12
|
+
from coco_graph._internal._typing import MISSING
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_optional_type(type_: Any) -> bool:
|
|
16
|
+
"""Check if a type is Optional."""
|
|
17
|
+
|
|
18
|
+
# Handle new union syntax (PEP 604): str | None
|
|
19
|
+
if isinstance(type_, types.UnionType):
|
|
20
|
+
return any(
|
|
21
|
+
arg is type(None) or _is_optional_type(arg) for arg in type_.__args__
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if hasattr(type_, "__origin__") and hasattr(type_, "__args__"):
|
|
25
|
+
origin = get_origin(type_)
|
|
26
|
+
if origin is Optional:
|
|
27
|
+
return True
|
|
28
|
+
if origin is Union:
|
|
29
|
+
return any(
|
|
30
|
+
arg is type(None) or _is_optional_type(arg) for arg in type_.__args__
|
|
31
|
+
)
|
|
32
|
+
if origin is Annotated:
|
|
33
|
+
return _is_optional_type(type_.__args__[0])
|
|
34
|
+
return origin is None
|
|
35
|
+
if hasattr(type_, "__bound__") and type_.__bound__ is not None:
|
|
36
|
+
return _is_optional_type(type_.__bound__)
|
|
37
|
+
return type_ is None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_required_type(type_: Any) -> bool | None:
|
|
41
|
+
"""Check if an annotation is marked as Required/NotRequired.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
- True if required
|
|
45
|
+
- False if not required
|
|
46
|
+
- None if not annotated with either
|
|
47
|
+
"""
|
|
48
|
+
origin = get_origin(type_)
|
|
49
|
+
if origin is Required:
|
|
50
|
+
return True
|
|
51
|
+
if origin is NotRequired:
|
|
52
|
+
return False
|
|
53
|
+
if origin is Annotated or getattr(origin, "__args__", None):
|
|
54
|
+
# See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated
|
|
55
|
+
return _is_required_type(type_.__args__[0])
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_readonly_type(type_: Any) -> bool:
|
|
60
|
+
"""Check if an annotation is marked as ReadOnly.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
- True if is read only
|
|
64
|
+
- False if not read only
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier
|
|
68
|
+
origin = get_origin(type_)
|
|
69
|
+
if origin is Annotated:
|
|
70
|
+
return _is_readonly_type(type_.__args__[0])
|
|
71
|
+
if origin is ReadOnly:
|
|
72
|
+
return True
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_DEFAULT_KEYS: frozenset[str] = frozenset()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_field_default(name: str, type_: Any, schema: type[Any]) -> Any:
|
|
80
|
+
"""Determine the default value for a field in a state schema.
|
|
81
|
+
|
|
82
|
+
This is based on:
|
|
83
|
+
If TypedDict:
|
|
84
|
+
- Required/NotRequired
|
|
85
|
+
- total=False -> everything optional
|
|
86
|
+
- Type annotation (Optional/Union[None])
|
|
87
|
+
"""
|
|
88
|
+
optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS)
|
|
89
|
+
irq = _is_required_type(type_)
|
|
90
|
+
if name in optional_keys:
|
|
91
|
+
# Either total=False or explicit NotRequired.
|
|
92
|
+
# No type annotation trumps this.
|
|
93
|
+
if irq:
|
|
94
|
+
# Unless it's earlier versions of python & explicit Required
|
|
95
|
+
return ...
|
|
96
|
+
return None
|
|
97
|
+
if irq is not None:
|
|
98
|
+
if irq:
|
|
99
|
+
# Handle Required[<type>]
|
|
100
|
+
# (we already handled NotRequired and total=False)
|
|
101
|
+
return ...
|
|
102
|
+
# Handle NotRequired[<type>] for earlier versions of python
|
|
103
|
+
return None
|
|
104
|
+
if dataclasses.is_dataclass(schema):
|
|
105
|
+
field_info = next(
|
|
106
|
+
(f for f in dataclasses.fields(schema) if f.name == name), None
|
|
107
|
+
)
|
|
108
|
+
if field_info:
|
|
109
|
+
if (
|
|
110
|
+
field_info.default is not dataclasses.MISSING
|
|
111
|
+
and field_info.default is not ...
|
|
112
|
+
):
|
|
113
|
+
return field_info.default
|
|
114
|
+
elif field_info.default_factory is not dataclasses.MISSING:
|
|
115
|
+
return field_info.default_factory()
|
|
116
|
+
# Note, we ignore ReadOnly attributes,
|
|
117
|
+
# as they don't make much sense. (we don't care if you mutate the state in your node)
|
|
118
|
+
# and mutating state in your node has no effect on our graph state.
|
|
119
|
+
# Base case is the annotation
|
|
120
|
+
if _is_optional_type(type_):
|
|
121
|
+
return None
|
|
122
|
+
return ...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_enhanced_type_hints(
|
|
126
|
+
type: type[Any],
|
|
127
|
+
) -> Generator[tuple[str, Any, Any, str | None], None, None]:
|
|
128
|
+
"""Attempt to extract default values and descriptions from provided type, used for config schema."""
|
|
129
|
+
for name, typ in get_type_hints(type).items():
|
|
130
|
+
default = None
|
|
131
|
+
description = None
|
|
132
|
+
|
|
133
|
+
# Pydantic models
|
|
134
|
+
try:
|
|
135
|
+
if hasattr(type, "model_fields") and name in type.model_fields:
|
|
136
|
+
field = type.model_fields[name]
|
|
137
|
+
|
|
138
|
+
if hasattr(field, "description") and field.description is not None:
|
|
139
|
+
description = field.description
|
|
140
|
+
|
|
141
|
+
if hasattr(field, "default") and field.default is not None:
|
|
142
|
+
default = field.default
|
|
143
|
+
if (
|
|
144
|
+
hasattr(default, "__class__")
|
|
145
|
+
and getattr(default.__class__, "__name__", "")
|
|
146
|
+
== "PydanticUndefinedType"
|
|
147
|
+
):
|
|
148
|
+
default = None
|
|
149
|
+
|
|
150
|
+
except (AttributeError, KeyError, TypeError):
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# TypedDict, dataclass
|
|
154
|
+
try:
|
|
155
|
+
if hasattr(type, "__dict__"):
|
|
156
|
+
type_dict = getattr(type, "__dict__")
|
|
157
|
+
|
|
158
|
+
if name in type_dict:
|
|
159
|
+
default = type_dict[name]
|
|
160
|
+
except (AttributeError, KeyError, TypeError):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
yield name, typ, default, description
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_update_as_tuples(input: Any, keys: Sequence[str]) -> list[tuple[str, Any]]:
|
|
167
|
+
"""Get Pydantic state update as a list of (key, value) tuples."""
|
|
168
|
+
if isinstance(input, BaseModel):
|
|
169
|
+
keep = input.model_fields_set
|
|
170
|
+
defaults = {k: v.default for k, v in type(input).model_fields.items()}
|
|
171
|
+
else:
|
|
172
|
+
keep = None
|
|
173
|
+
defaults = {}
|
|
174
|
+
|
|
175
|
+
# NOTE: This behavior for Pydantic is somewhat inelegant,
|
|
176
|
+
# but we keep around for backwards compatibility
|
|
177
|
+
# if input is a Pydantic model, only update values
|
|
178
|
+
# that are different from the default values or in the keep set
|
|
179
|
+
return [
|
|
180
|
+
(k, value)
|
|
181
|
+
for k in keys
|
|
182
|
+
if (value := getattr(input, k, MISSING)) is not MISSING
|
|
183
|
+
and (
|
|
184
|
+
value is not None
|
|
185
|
+
or defaults.get(k, MISSING) is not None
|
|
186
|
+
or (keep is not None and k in keep)
|
|
187
|
+
)
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
ANNOTATED_KEYS_CACHE: weakref.WeakKeyDictionary[type[Any], tuple[str, ...]] = (
|
|
192
|
+
weakref.WeakKeyDictionary()
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_cached_annotated_keys(obj: type[Any]) -> tuple[str, ...]:
|
|
197
|
+
"""Return cached annotated keys for a Python class."""
|
|
198
|
+
if obj in ANNOTATED_KEYS_CACHE:
|
|
199
|
+
return ANNOTATED_KEYS_CACHE[obj]
|
|
200
|
+
if isinstance(obj, type):
|
|
201
|
+
keys: list[str] = []
|
|
202
|
+
for base in reversed(obj.__mro__):
|
|
203
|
+
ann = base.__dict__.get("__annotations__")
|
|
204
|
+
# In Python 3.14+, Pydantic models use descriptors for __annotations__
|
|
205
|
+
# so we need to fall back to getattr if __dict__.get returns None
|
|
206
|
+
if ann is None:
|
|
207
|
+
ann = getattr(base, "__annotations__", None)
|
|
208
|
+
if ann is None or isinstance(ann, types.GetSetDescriptorType):
|
|
209
|
+
continue
|
|
210
|
+
keys.extend(ann.keys())
|
|
211
|
+
return ANNOTATED_KEYS_CACHE.setdefault(obj, tuple(keys))
|
|
212
|
+
else:
|
|
213
|
+
raise TypeError(f"Expected a type, got {type(obj)}. ")
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
import contextvars
|
|
6
|
+
import inspect
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
from collections.abc import Awaitable, Coroutine, Generator
|
|
10
|
+
from typing import TypeVar, cast
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
AnyFuture = asyncio.Future | concurrent.futures.Future
|
|
14
|
+
|
|
15
|
+
CONTEXT_NOT_SUPPORTED = sys.version_info < (3, 11)
|
|
16
|
+
EAGER_NOT_SUPPORTED = sys.version_info < (3, 12)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_loop(fut: asyncio.Future) -> asyncio.AbstractEventLoop:
|
|
20
|
+
# Tries to call Future.get_loop() if it's available.
|
|
21
|
+
# Otherwise fallbacks to using the old '_loop' property.
|
|
22
|
+
try:
|
|
23
|
+
get_loop = fut.get_loop
|
|
24
|
+
except AttributeError:
|
|
25
|
+
pass
|
|
26
|
+
else:
|
|
27
|
+
return get_loop()
|
|
28
|
+
return fut._loop
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _convert_future_exc(exc: BaseException) -> BaseException:
|
|
32
|
+
exc_class = type(exc)
|
|
33
|
+
if exc_class is concurrent.futures.CancelledError:
|
|
34
|
+
return asyncio.CancelledError(*exc.args)
|
|
35
|
+
elif exc_class is concurrent.futures.TimeoutError:
|
|
36
|
+
return asyncio.TimeoutError(*exc.args)
|
|
37
|
+
elif exc_class is concurrent.futures.InvalidStateError:
|
|
38
|
+
return asyncio.InvalidStateError(*exc.args)
|
|
39
|
+
else:
|
|
40
|
+
return exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _set_concurrent_future_state(
|
|
44
|
+
concurrent: concurrent.futures.Future,
|
|
45
|
+
source: AnyFuture,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Copy state from a future to a concurrent.futures.Future."""
|
|
48
|
+
assert source.done()
|
|
49
|
+
if source.cancelled():
|
|
50
|
+
concurrent.cancel()
|
|
51
|
+
if not concurrent.set_running_or_notify_cancel():
|
|
52
|
+
return
|
|
53
|
+
exception = source.exception()
|
|
54
|
+
if exception is not None:
|
|
55
|
+
concurrent.set_exception(_convert_future_exc(exception))
|
|
56
|
+
else:
|
|
57
|
+
result = source.result()
|
|
58
|
+
concurrent.set_result(result)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _copy_future_state(source: AnyFuture, dest: asyncio.Future) -> None:
|
|
62
|
+
"""Internal helper to copy state from another Future.
|
|
63
|
+
|
|
64
|
+
The other Future may be a concurrent.futures.Future.
|
|
65
|
+
"""
|
|
66
|
+
if dest.done():
|
|
67
|
+
return
|
|
68
|
+
assert source.done()
|
|
69
|
+
if dest.cancelled():
|
|
70
|
+
return
|
|
71
|
+
if source.cancelled():
|
|
72
|
+
dest.cancel()
|
|
73
|
+
else:
|
|
74
|
+
exception = source.exception()
|
|
75
|
+
if exception is not None:
|
|
76
|
+
dest.set_exception(_convert_future_exc(exception))
|
|
77
|
+
else:
|
|
78
|
+
result = source.result()
|
|
79
|
+
dest.set_result(result)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _chain_future(source: AnyFuture, destination: AnyFuture) -> None:
|
|
83
|
+
"""Chain two futures so that when one completes, so does the other.
|
|
84
|
+
|
|
85
|
+
The result (or exception) of source will be copied to destination.
|
|
86
|
+
If destination is cancelled, source gets cancelled too.
|
|
87
|
+
Compatible with both asyncio.Future and concurrent.futures.Future.
|
|
88
|
+
"""
|
|
89
|
+
if not asyncio.isfuture(source) and not isinstance(
|
|
90
|
+
source, concurrent.futures.Future
|
|
91
|
+
):
|
|
92
|
+
raise TypeError("A future is required for source argument")
|
|
93
|
+
if not asyncio.isfuture(destination) and not isinstance(
|
|
94
|
+
destination, concurrent.futures.Future
|
|
95
|
+
):
|
|
96
|
+
raise TypeError("A future is required for destination argument")
|
|
97
|
+
source_loop = _get_loop(source) if asyncio.isfuture(source) else None
|
|
98
|
+
dest_loop = _get_loop(destination) if asyncio.isfuture(destination) else None
|
|
99
|
+
|
|
100
|
+
def _set_state(future: AnyFuture, other: AnyFuture) -> None:
|
|
101
|
+
if asyncio.isfuture(future):
|
|
102
|
+
_copy_future_state(other, future)
|
|
103
|
+
else:
|
|
104
|
+
_set_concurrent_future_state(future, other)
|
|
105
|
+
|
|
106
|
+
def _call_check_cancel(destination: AnyFuture) -> None:
|
|
107
|
+
if destination.cancelled():
|
|
108
|
+
if source_loop is None or source_loop is dest_loop:
|
|
109
|
+
source.cancel()
|
|
110
|
+
else:
|
|
111
|
+
source_loop.call_soon_threadsafe(source.cancel)
|
|
112
|
+
|
|
113
|
+
def _call_set_state(source: AnyFuture) -> None:
|
|
114
|
+
if destination.cancelled() and dest_loop is not None and dest_loop.is_closed():
|
|
115
|
+
return
|
|
116
|
+
if dest_loop is None or dest_loop is source_loop:
|
|
117
|
+
_set_state(destination, source)
|
|
118
|
+
else:
|
|
119
|
+
if dest_loop.is_closed():
|
|
120
|
+
return
|
|
121
|
+
dest_loop.call_soon_threadsafe(_set_state, destination, source)
|
|
122
|
+
|
|
123
|
+
destination.add_done_callback(_call_check_cancel)
|
|
124
|
+
source.add_done_callback(_call_set_state)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def chain_future(source: AnyFuture, destination: AnyFuture) -> AnyFuture:
|
|
128
|
+
# adapted from asyncio.run_coroutine_threadsafe
|
|
129
|
+
try:
|
|
130
|
+
_chain_future(source, destination)
|
|
131
|
+
return destination
|
|
132
|
+
except (SystemExit, KeyboardInterrupt):
|
|
133
|
+
raise
|
|
134
|
+
except BaseException as exc:
|
|
135
|
+
if isinstance(destination, concurrent.futures.Future):
|
|
136
|
+
if destination.set_running_or_notify_cancel():
|
|
137
|
+
destination.set_exception(exc)
|
|
138
|
+
else:
|
|
139
|
+
destination.set_exception(exc)
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _ensure_future(
|
|
144
|
+
coro_or_future: Coroutine[None, None, T] | Awaitable[T],
|
|
145
|
+
*,
|
|
146
|
+
loop: asyncio.AbstractEventLoop,
|
|
147
|
+
name: str | None = None,
|
|
148
|
+
context: contextvars.Context | None = None,
|
|
149
|
+
lazy: bool = True,
|
|
150
|
+
) -> asyncio.Task[T]:
|
|
151
|
+
called_wrap_awaitable = False
|
|
152
|
+
if not asyncio.iscoroutine(coro_or_future):
|
|
153
|
+
if inspect.isawaitable(coro_or_future):
|
|
154
|
+
coro_or_future = cast(
|
|
155
|
+
Coroutine[None, None, T], _wrap_awaitable(coro_or_future)
|
|
156
|
+
)
|
|
157
|
+
called_wrap_awaitable = True
|
|
158
|
+
else:
|
|
159
|
+
raise TypeError(
|
|
160
|
+
"An asyncio.Future, a coroutine or an awaitable is required."
|
|
161
|
+
f" Got {type(coro_or_future).__name__} instead."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
if CONTEXT_NOT_SUPPORTED:
|
|
166
|
+
return loop.create_task(coro_or_future, name=name)
|
|
167
|
+
elif EAGER_NOT_SUPPORTED or lazy:
|
|
168
|
+
return loop.create_task(coro_or_future, name=name, context=context)
|
|
169
|
+
else:
|
|
170
|
+
return asyncio.eager_task_factory(
|
|
171
|
+
loop, coro_or_future, name=name, context=context
|
|
172
|
+
)
|
|
173
|
+
except RuntimeError:
|
|
174
|
+
if not called_wrap_awaitable:
|
|
175
|
+
coro_or_future.close()
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@types.coroutine
|
|
180
|
+
def _wrap_awaitable(awaitable: Awaitable[T]) -> Generator[None, None, T]:
|
|
181
|
+
"""Helper for asyncio.ensure_future().
|
|
182
|
+
|
|
183
|
+
Wraps awaitable (an object with __await__) into a coroutine
|
|
184
|
+
that will later be wrapped in a Task by ensure_future().
|
|
185
|
+
"""
|
|
186
|
+
return (yield from awaitable.__await__())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_coroutine_threadsafe(
|
|
190
|
+
coro: Coroutine[None, None, T],
|
|
191
|
+
loop: asyncio.AbstractEventLoop,
|
|
192
|
+
*,
|
|
193
|
+
lazy: bool,
|
|
194
|
+
name: str | None = None,
|
|
195
|
+
context: contextvars.Context | None = None,
|
|
196
|
+
) -> asyncio.Future[T]:
|
|
197
|
+
"""Submit a coroutine object to a given event loop.
|
|
198
|
+
|
|
199
|
+
Return an asyncio.Future to access the result.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
if asyncio._get_running_loop() is loop:
|
|
203
|
+
return _ensure_future(coro, loop=loop, name=name, context=context, lazy=lazy)
|
|
204
|
+
else:
|
|
205
|
+
future: asyncio.Future[T] = asyncio.Future(loop=loop)
|
|
206
|
+
|
|
207
|
+
def callback() -> None:
|
|
208
|
+
try:
|
|
209
|
+
chain_future(
|
|
210
|
+
_ensure_future(coro, loop=loop, name=name, context=context),
|
|
211
|
+
future,
|
|
212
|
+
)
|
|
213
|
+
except (SystemExit, KeyboardInterrupt):
|
|
214
|
+
raise
|
|
215
|
+
except BaseException as exc:
|
|
216
|
+
future.set_exception(exc)
|
|
217
|
+
raise
|
|
218
|
+
|
|
219
|
+
loop.call_soon_threadsafe(callback, context=context)
|
|
220
|
+
return future
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import typing
|
|
5
|
+
import warnings
|
|
6
|
+
from contextlib import nullcontext
|
|
7
|
+
from dataclasses import is_dataclass
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
cast,
|
|
12
|
+
overload,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from pydantic import (
|
|
16
|
+
BaseModel,
|
|
17
|
+
ConfigDict,
|
|
18
|
+
Field,
|
|
19
|
+
RootModel,
|
|
20
|
+
)
|
|
21
|
+
from pydantic import (
|
|
22
|
+
create_model as _create_model_base,
|
|
23
|
+
)
|
|
24
|
+
from pydantic.fields import FieldInfo
|
|
25
|
+
from pydantic.json_schema import (
|
|
26
|
+
DEFAULT_REF_TEMPLATE,
|
|
27
|
+
GenerateJsonSchema,
|
|
28
|
+
JsonSchemaMode,
|
|
29
|
+
)
|
|
30
|
+
from typing_extensions import TypedDict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
def get_fields(model: type[BaseModel]) -> dict[str, FieldInfo]: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def get_fields(model: BaseModel) -> dict[str, FieldInfo]: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_fields(
|
|
42
|
+
model: type[BaseModel] | BaseModel,
|
|
43
|
+
) -> dict[str, FieldInfo]:
|
|
44
|
+
"""Get the field names of a Pydantic model."""
|
|
45
|
+
if hasattr(model, "model_fields"):
|
|
46
|
+
return model.model_fields
|
|
47
|
+
|
|
48
|
+
if hasattr(model, "__fields__"):
|
|
49
|
+
return model.__fields__
|
|
50
|
+
msg = f"Expected a Pydantic model. Got {type(model)}"
|
|
51
|
+
raise TypeError(msg)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_SchemaConfig = ConfigDict(
|
|
55
|
+
arbitrary_types_allowed=True, frozen=True, protected_namespaces=()
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
NO_DEFAULT = object()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _create_root_model(
|
|
62
|
+
name: str,
|
|
63
|
+
type_: Any,
|
|
64
|
+
module_name: str | None = None,
|
|
65
|
+
default_: object = NO_DEFAULT,
|
|
66
|
+
) -> type[BaseModel]:
|
|
67
|
+
"""Create a base class."""
|
|
68
|
+
|
|
69
|
+
def schema(
|
|
70
|
+
cls: type[BaseModel],
|
|
71
|
+
by_alias: bool = True,
|
|
72
|
+
ref_template: str = DEFAULT_REF_TEMPLATE,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
# Complains about schema not being defined in superclass
|
|
75
|
+
schema_ = super(cls, cls).schema( # type: ignore[misc]
|
|
76
|
+
by_alias=by_alias, ref_template=ref_template
|
|
77
|
+
)
|
|
78
|
+
schema_["title"] = name
|
|
79
|
+
return schema_
|
|
80
|
+
|
|
81
|
+
def model_json_schema(
|
|
82
|
+
cls: type[BaseModel],
|
|
83
|
+
by_alias: bool = True,
|
|
84
|
+
ref_template: str = DEFAULT_REF_TEMPLATE,
|
|
85
|
+
schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
|
|
86
|
+
mode: JsonSchemaMode = "validation",
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
# Complains about model_json_schema not being defined in superclass
|
|
89
|
+
schema_ = super(cls, cls).model_json_schema( # type: ignore[misc]
|
|
90
|
+
by_alias=by_alias,
|
|
91
|
+
ref_template=ref_template,
|
|
92
|
+
schema_generator=schema_generator,
|
|
93
|
+
mode=mode,
|
|
94
|
+
)
|
|
95
|
+
schema_["title"] = name
|
|
96
|
+
return schema_
|
|
97
|
+
|
|
98
|
+
base_class_attributes = {
|
|
99
|
+
"__annotations__": {"root": type_},
|
|
100
|
+
"model_config": ConfigDict(arbitrary_types_allowed=True),
|
|
101
|
+
"schema": classmethod(schema),
|
|
102
|
+
"model_json_schema": classmethod(model_json_schema),
|
|
103
|
+
"__module__": module_name or "coco_core.runnables.utils",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if default_ is not NO_DEFAULT:
|
|
107
|
+
base_class_attributes["root"] = default_
|
|
108
|
+
with warnings.catch_warnings():
|
|
109
|
+
custom_root_type = type(name, (RootModel,), base_class_attributes)
|
|
110
|
+
return cast("type[BaseModel]", custom_root_type)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@lru_cache(maxsize=256)
|
|
114
|
+
def _create_root_model_cached(
|
|
115
|
+
model_name: str,
|
|
116
|
+
type_: Any,
|
|
117
|
+
*,
|
|
118
|
+
module_name: str | None = None,
|
|
119
|
+
default_: object = NO_DEFAULT,
|
|
120
|
+
) -> type[BaseModel]:
|
|
121
|
+
return _create_root_model(
|
|
122
|
+
model_name, type_, default_=default_, module_name=module_name
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@lru_cache(maxsize=256)
|
|
127
|
+
def _create_model_cached(
|
|
128
|
+
model_name: str,
|
|
129
|
+
/,
|
|
130
|
+
**field_definitions: Any,
|
|
131
|
+
) -> type[BaseModel]:
|
|
132
|
+
return _create_model_base(
|
|
133
|
+
model_name,
|
|
134
|
+
__config__=_SchemaConfig,
|
|
135
|
+
**_remap_field_definitions(field_definitions),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Reserved names should capture all the `public` names / methods that are
|
|
140
|
+
# used by BaseModel internally. This will keep the reserved names up-to-date.
|
|
141
|
+
# For reference, the reserved names are:
|
|
142
|
+
# "construct", "copy", "dict", "from_orm", "json", "parse_file", "parse_obj",
|
|
143
|
+
# "parse_raw", "schema", "schema_json", "update_forward_refs", "validate",
|
|
144
|
+
# "model_computed_fields", "model_config", "model_construct", "model_copy",
|
|
145
|
+
# "model_dump", "model_dump_json", "model_extra", "model_fields",
|
|
146
|
+
# "model_fields_set", "model_json_schema", "model_parametrized_name",
|
|
147
|
+
# "model_post_init", "model_rebuild", "model_validate", "model_validate_json",
|
|
148
|
+
# "model_validate_strings"
|
|
149
|
+
_RESERVED_NAMES = {key for key in dir(BaseModel) if not key.startswith("_")}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _remap_field_definitions(field_definitions: dict[str, Any]) -> dict[str, Any]:
|
|
153
|
+
"""This remaps fields to avoid colliding with internal pydantic fields."""
|
|
154
|
+
|
|
155
|
+
remapped = {}
|
|
156
|
+
for key, value in field_definitions.items():
|
|
157
|
+
if key.startswith("_") or key in _RESERVED_NAMES:
|
|
158
|
+
# Let's add a prefix to avoid colliding with internal pydantic fields
|
|
159
|
+
if isinstance(value, FieldInfo):
|
|
160
|
+
msg = (
|
|
161
|
+
f"Remapping for fields starting with '_' or fields with a name "
|
|
162
|
+
f"matching a reserved name {_RESERVED_NAMES} is not supported if "
|
|
163
|
+
f" the field is a pydantic Field instance. Got {key}."
|
|
164
|
+
)
|
|
165
|
+
raise NotImplementedError(msg)
|
|
166
|
+
type_, default_ = value
|
|
167
|
+
remapped[f"private_{key}"] = (
|
|
168
|
+
type_,
|
|
169
|
+
Field(
|
|
170
|
+
default=default_,
|
|
171
|
+
alias=key,
|
|
172
|
+
serialization_alias=key,
|
|
173
|
+
title=key.lstrip("_").replace("_", " ").title(),
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
remapped[key] = value
|
|
178
|
+
return remapped
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def create_model(
|
|
182
|
+
model_name: str,
|
|
183
|
+
*,
|
|
184
|
+
field_definitions: dict[str, Any] | None = None,
|
|
185
|
+
root: Any | None = None,
|
|
186
|
+
) -> type[BaseModel]:
|
|
187
|
+
"""Create a pydantic model with the given field definitions.
|
|
188
|
+
|
|
189
|
+
Attention:
|
|
190
|
+
Please do not use outside of coco packages. This API
|
|
191
|
+
is subject to change at any time.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
model_name: The name of the model.
|
|
195
|
+
module_name: The name of the module where the model is defined.
|
|
196
|
+
This is used by Pydantic to resolve any forward references.
|
|
197
|
+
field_definitions: The field definitions for the model.
|
|
198
|
+
root: Type for a root model (RootModel)
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Type[BaseModel]: The created model.
|
|
202
|
+
"""
|
|
203
|
+
field_definitions = field_definitions or {}
|
|
204
|
+
|
|
205
|
+
if root:
|
|
206
|
+
if field_definitions:
|
|
207
|
+
msg = (
|
|
208
|
+
"When specifying __root__ no other "
|
|
209
|
+
f"fields should be provided. Got {field_definitions}"
|
|
210
|
+
)
|
|
211
|
+
raise NotImplementedError(msg)
|
|
212
|
+
|
|
213
|
+
if isinstance(root, tuple):
|
|
214
|
+
kwargs = {"type_": root[0], "default_": root[1]}
|
|
215
|
+
else:
|
|
216
|
+
kwargs = {"type_": root}
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
named_root_model = _create_root_model_cached(model_name, **kwargs)
|
|
220
|
+
except TypeError:
|
|
221
|
+
# something in the arguments into _create_root_model_cached is not hashable
|
|
222
|
+
named_root_model = _create_root_model(
|
|
223
|
+
model_name,
|
|
224
|
+
**kwargs,
|
|
225
|
+
)
|
|
226
|
+
return named_root_model
|
|
227
|
+
|
|
228
|
+
# No root, just field definitions
|
|
229
|
+
names = set(field_definitions.keys())
|
|
230
|
+
|
|
231
|
+
capture_warnings = False
|
|
232
|
+
|
|
233
|
+
for name in names:
|
|
234
|
+
# Also if any non-reserved name is used (e.g., model_id or model_name)
|
|
235
|
+
if name.startswith("model"):
|
|
236
|
+
capture_warnings = True
|
|
237
|
+
|
|
238
|
+
with warnings.catch_warnings() if capture_warnings else nullcontext():
|
|
239
|
+
if capture_warnings:
|
|
240
|
+
warnings.filterwarnings(action="ignore")
|
|
241
|
+
try:
|
|
242
|
+
return _create_model_cached(model_name, **field_definitions)
|
|
243
|
+
except TypeError:
|
|
244
|
+
# something in field definitions is not hashable
|
|
245
|
+
return _create_model_base(
|
|
246
|
+
model_name,
|
|
247
|
+
__config__=_SchemaConfig,
|
|
248
|
+
**_remap_field_definitions(field_definitions),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def is_supported_by_pydantic(type_: Any) -> bool:
|
|
253
|
+
"""Check if a given "complex" type is supported by pydantic.
|
|
254
|
+
|
|
255
|
+
This will return False for primitive types like int, str, etc.
|
|
256
|
+
|
|
257
|
+
The check is meant for container types like dataclasses, TypedDicts, etc.
|
|
258
|
+
"""
|
|
259
|
+
if is_dataclass(type_):
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
if isinstance(type_, type) and issubclass(type_, BaseModel):
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
if hasattr(type_, "__orig_bases__"):
|
|
266
|
+
for base in type_.__orig_bases__:
|
|
267
|
+
if base is TypedDict:
|
|
268
|
+
return True
|
|
269
|
+
elif base is typing.TypedDict: # noqa: TID251
|
|
270
|
+
# ignoring TID251 since it's OK to use typing.TypedDict in this case.
|
|
271
|
+
# Pydantic supports typing.TypedDict from Python 3.12
|
|
272
|
+
# For older versions, only typing_extensions.TypedDict is supported.
|
|
273
|
+
if sys.version_info >= (3, 12):
|
|
274
|
+
return True
|
|
275
|
+
return False
|