feat(types): use cloudpickle for callable serialization

This commit is contained in:
Greyson Lalonde
2026-03-07 17:48:59 -05:00
parent 31b8a0989a
commit f5116004db
3 changed files with 28 additions and 26 deletions

View File

@@ -43,6 +43,7 @@ dependencies = [
"uv~=0.9.13",
"aiosqlite~=0.21.0",
"lancedb>=0.29.2",
"cloudpickle~=3.1.2",
]
[project.urls]

View File

@@ -1,45 +1,35 @@
"""Serializable callable type for Pydantic models."""
"""Serializable callable type for Pydantic models.
All callables (named functions, lambdas, closures, methods) are serialized
via ``cloudpickle`` + base64. On deserialization the base64 payload is
decoded and unpickled back into a live callable.
"""
from __future__ import annotations
import base64
from collections.abc import Callable
import importlib
from typing import Annotated, Any
import cloudpickle # type: ignore[import-untyped]
from pydantic import BeforeValidator, PlainSerializer, WithJsonSchema
def _deserialize_callable(v: str | Callable[..., Any]) -> Callable[..., Any]:
"""Deserialize a dotted import path to a callable, or pass through if already callable."""
"""Deserialize a base64-encoded cloudpickle payload, or pass through if already callable."""
if isinstance(v, str):
module_path, _, name = v.rpartition(".")
if not module_path:
raise ValueError(f"Invalid callable path: {v!r} (expected 'module.name')")
module = importlib.import_module(module_path)
obj: Callable[..., Any] = getattr(module, name)
obj = cloudpickle.loads(base64.b85decode(v))
if not callable(obj):
raise ValueError(f"{v!r} resolved to {type(obj).__name__}, not a callable")
return obj
raise ValueError(
f"Deserialized object is {type(obj).__name__}, not a callable"
)
return obj # type: ignore[no-any-return]
return v
def _serialize_callable(v: Callable[..., Any]) -> str:
"""Serialize a callable to its dotted import path."""
module = getattr(v, "__module__", None)
qualname = getattr(v, "__qualname__", None)
name = getattr(v, "__name__", None)
if not module or not name:
raise ValueError(
f"Cannot serialize {v!r}: missing __module__ or __name__. "
"Only top-level named functions are serializable."
)
if qualname and "<" in qualname:
raise ValueError(
f"Cannot serialize {v!r}: lambdas and nested functions are not serializable. "
"Use a top-level named function instead."
)
return f"{module}.{qualname or name}"
"""Serialize any callable to a base64-encoded cloudpickle payload."""
return base64.b85encode(cloudpickle.dumps(v)).decode("ascii")
SerializableCallable = Annotated[

11
uv.lock generated
View File

@@ -911,6 +911,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
]
[[package]]
name = "cloudpickle"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -1097,6 +1106,7 @@ dependencies = [
{ name = "appdirs" },
{ name = "chromadb" },
{ name = "click" },
{ name = "cloudpickle" },
{ name = "httpx" },
{ name = "instructor" },
{ name = "json-repair" },
@@ -1193,6 +1203,7 @@ requires-dist = [
{ name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.40.45" },
{ name = "chromadb", specifier = "~=1.1.0" },
{ name = "click", specifier = "~=8.1.7" },
{ name = "cloudpickle", specifier = "~=3.1.2" },
{ name = "crewai-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" },
{ name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" },
{ name = "docling", marker = "extra == 'docling'", specifier = "~=2.75.0" },