Files
lancedb/python/python/lancedb/functions.py
T
Xuanwo a588208de6 feat: add scalar function authoring and catalog client (#3991)
## Problem

The canonical Function wire values and typed remote Job contract do not
yet provide a Python authoring surface or catalog client, so users
cannot package a scalar callable, register it, or reopen the exact
immutable Function version.

## Behavior

This adds scalar-only `@udf` authoring with deterministic annotation or
explicit Arrow schema validation, content-addressed Python artifacts,
and an internal scalar-to-Arrow-batch adapter descriptor. Registration
payloads model non-secret environment values and secret names only.

Remote connections can submit `create_function_async` and receive a
typed `Job<FunctionVersion>`, then reopen that exact version by name and
version ID. Synchronous connections can call `create_function` to submit
and wait for the immutable version in one operation. Local Function
catalog operations return a stable `NotSupported` error. Shared
Rust/Python golden payloads and mocked catalog responses freeze the
request, typed terminal result, and exact lookup contract.

## Validation

- Rust formatting, remote check, clippy, and focused LDB-1/LDB-2 tests
- Python formatting, lint, and focused LDB-1/LDB-2 tests
- Python API documentation build
2026-08-21 17:19:13 +08:00

912 lines
30 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Canonical values exchanged with LanceDB Enterprise Function services.
These immutable models contain client/wire state only. Catalog persistence,
environment bake, secret resolution, and execution are owned by Sophon.
"""
from __future__ import annotations
import ast
import base64
import functools
import hashlib
import inspect
import json
import math
import re
import sys
import textwrap
import types
from collections.abc import Mapping
from datetime import date, datetime
from typing import (
Annotated,
Any,
Callable,
Optional,
Union,
get_args,
get_origin,
get_type_hints,
overload,
)
import pyarrow as pa
from pydantic import (
BaseModel,
ConfigDict,
Field,
conint,
field_validator,
model_validator,
)
_Int32 = conint(strict=True, ge=-(2**31), le=2**31 - 1)
_UInt32 = conint(strict=True, ge=0, le=2**32 - 1)
_UInt64 = conint(strict=True, ge=0, le=2**64 - 1)
class _FrozenDict(dict):
def _immutable(self, *args, **kwargs):
raise TypeError("remote canonical values are immutable")
__setitem__ = _immutable
__delitem__ = _immutable
clear = _immutable
pop = _immutable
popitem = _immutable
setdefault = _immutable
update = _immutable
def __ior__(self, other):
self._immutable()
def _freeze_value(value):
if isinstance(value, Mapping):
return _FrozenDict({key: _freeze_value(child) for key, child in value.items()})
if isinstance(value, (list, tuple)):
return tuple(_freeze_value(child) for child in value)
return value
def _validate_literal(value):
if isinstance(value, float):
raise ValueError(
"floating-point Function literals are not part of the Slice 1 "
"canonical wire contract"
)
if isinstance(value, int) and not isinstance(value, bool):
if not -(2**63) <= value <= 2**64 - 1:
raise ValueError(
"Function integer literal is outside the canonical JSON range"
)
elif isinstance(value, Mapping):
for child in value.values():
_validate_literal(child)
elif isinstance(value, (list, tuple)):
for child in value:
_validate_literal(child)
return value
def _known_wire_value(value):
if isinstance(value, _RemoteValue):
return value._known_dict()
if isinstance(value, Mapping):
return {key: _known_wire_value(child) for key, child in value.items()}
if isinstance(value, (list, tuple)):
return [_known_wire_value(child) for child in value]
return value
class _RemoteValue(BaseModel):
model_config = ConfigDict(extra="ignore", frozen=True)
@model_validator(mode="after")
def _freeze_mappings(self):
for name, value in self.__dict__.items():
object.__setattr__(self, name, _freeze_value(value))
return self
@classmethod
def from_json(cls, payload: str):
return cls.model_validate_json(payload)
def _known_dict(self) -> dict[str, Any]:
known = {}
for name, field in self.__class__.model_fields.items():
value = getattr(self, name)
if value is None:
continue
if not field.is_required():
default_factory = field.default_factory
if default_factory is not None and value == default_factory():
continue
if default_factory is None and value == field.default:
continue
known[name] = _known_wire_value(value)
return known
def _copy(self, *, update: Mapping[str, Any]):
update = {name: _freeze_value(value) for name, value in update.items()}
return self.model_copy(update=update)
def to_canonical_json(self) -> str:
return json.dumps(
self._known_dict(),
ensure_ascii=False,
allow_nan=False,
sort_keys=True,
separators=(",", ":"),
)
class _OpenRemoteValue(_RemoteValue):
"""Forward-readable value whose extras stay out of canonical encoding."""
model_config = ConfigDict(extra="allow", frozen=True)
def _unknown_field_names(self) -> set[str]:
return set((self.__pydantic_extra__ or {}).keys())
class FunctionArtifact(_RemoteValue):
"""Content-addressed Python artifact identity."""
kind: str
digest: str
entrypoint: str
class FunctionArtifactContent(_RemoteValue):
"""Encoded artifact bytes uploaded during remote registration."""
encoding: str
data: str
class PythonAdapterSpec(_RemoteValue):
"""Internal scalar-callable to Arrow-batch adapter selection."""
kind: str
version: _UInt32
class FunctionArtifactRequest(_RemoteValue):
"""Source artifact uploaded while registering a Function."""
kind: str
digest: str
entrypoint: str
content: FunctionArtifactContent
adapter: PythonAdapterSpec
class FunctionParameter(_RemoteValue):
name: str
arrow_type: str
nullable: bool
class FunctionResultField(_OpenRemoteValue):
name: str
arrow_type: str
nullable: bool
class FunctionOutput(_OpenRemoteValue):
"""Scalar or ordered named-struct output; unknown kinds remain decodable."""
kind: str
arrow_type: Optional[str] = None
nullable: Optional[bool] = None
fields: tuple[FunctionResultField, ...] = ()
class FunctionSignature(_RemoteValue):
inputs: tuple[FunctionParameter, ...]
output: FunctionOutput
class PythonEnvironmentSpec(_RemoteValue):
"""One Sophon-managed Python environment source."""
kind: str
packages: tuple[str, ...] = ()
path: Optional[str] = None
modules: tuple[str, ...] = ()
image: Optional[str] = None
class PythonRuntimeSpec(_RemoteValue):
"""Remote runtime definition with non-secret environment values.
V1 supports ``kind="python"``. Newer runtime kinds remain readable, while
their unknown payload fields are intentionally not retained by the client.
"""
kind: str
python_version: Optional[str] = None
environment: Optional[PythonEnvironmentSpec] = None
env: Optional[Mapping[str, str]] = None
@model_validator(mode="after")
def _validate_runtime_kind(self):
if self.kind == "python":
if self.python_version is None:
raise ValueError("python runtime requires python_version")
if self.environment is None:
raise ValueError("python runtime requires environment")
else:
object.__setattr__(self, "python_version", None)
object.__setattr__(self, "environment", None)
object.__setattr__(self, "env", None)
return self
class FunctionVersion(_RemoteValue):
"""An exact immutable Function version returned by Enterprise.
Scheduling resources, priority, concurrency, and retry policy belong to
the submitting Job and are not part of this identity.
"""
name: str
version: str
artifact: FunctionArtifact
signature: FunctionSignature
runtime: PythonRuntimeSpec
runtime_digest: str
environment_digest: str
required_secrets: tuple[str, ...] = ()
created_at: str
class FunctionRegistrationRequest(_RemoteValue):
"""Stable remote registration envelope produced by :func:`udf`.
Only secret names are represented. Secret values are resolved inside the
remote service and have no client request field.
"""
name: str
artifact: FunctionArtifactRequest
signature: FunctionSignature
runtime: PythonRuntimeSpec
required_secrets: tuple[str, ...] = ()
class FunctionVersionRef(_OpenRemoteValue):
name: str
version: str
class ApplicationInput(_OpenRemoteValue):
"""One parameter value.
Slice 1 freezes integers, strings, booleans, nulls, arrays, and objects.
Floating-point literal encoding is deferred until Python authoring is
introduced with a language-neutral numeric representation.
"""
parameter: str
kind: str
value: Any
@field_validator("value")
@classmethod
def _validate_value(cls, value):
return _validate_literal(value)
class FunctionApplication(_OpenRemoteValue):
"""Immutable pre-declaration application of an exact Function version."""
function: FunctionVersionRef
inputs: tuple[ApplicationInput, ...]
output: FunctionOutput
group_id: str
columns: Mapping[str, str] = Field(default_factory=dict)
def _known_dict(self) -> dict[str, Any]:
value = super()._known_dict()
for name in self._unknown_field_names():
value.pop(name, None)
return value
def _ensure_declarable(self) -> None:
unknown = {f"application.{name}" for name in self._unknown_field_names()}
unknown.update(
f"function.{name}" for name in self.function._unknown_field_names()
)
for index, input_value in enumerate(self.inputs):
unknown.update(
f"inputs[{index}].{name}" for name in input_value._unknown_field_names()
)
unknown.update(f"output.{name}" for name in self.output._unknown_field_names())
for index, field in enumerate(self.output.fields):
unknown.update(
f"output.fields[{index}].{name}"
for name in field._unknown_field_names()
)
if unknown:
raise ValueError(
"Function application contains fields from a newer contract: "
f"{sorted(unknown)!r}"
)
def rename(self, *, columns: Mapping[str, str]) -> FunctionApplication:
"""Return a copy with result-field to table-column aliases."""
if self.output.kind != "named_struct":
raise ValueError("rename(columns=...) requires a named-struct application")
result_fields = {field.name for field in self.output.fields}
unknown = set(columns) - result_fields
if unknown:
raise ValueError(f"unknown Function result fields: {sorted(unknown)!r}")
merged = dict(self.columns)
merged.update(columns)
destinations = tuple(
merged.get(field.name, field.name) for field in self.output.fields
)
if len(set(destinations)) != len(destinations):
raise ValueError("FunctionApplication rename destinations must be unique")
return self._copy(update={"columns": merged})
class InputBinding(_RemoteValue):
parameter: str
field_id: _Int32
field_path: str
arrow_type: str
nullable: bool
class OutputMapping(_RemoteValue):
"""One stable result-field mapping.
Assignment state is outside the Slice 1 client contract. During the NULL
transition Lance exposes no public cell-flag identifier to persist here.
"""
result_field: str
output_name: str
output_field_id: _Int32
output_ordinal: _UInt32
arrow_type: str
nullable: bool
class FunctionBinding(_RemoteValue):
"""Immutable grouped binding persisted by the Enterprise table service."""
binding_id: str
revision: _UInt64
function: FunctionVersionRef
group_id: str
inputs: tuple[InputBinding, ...]
outputs: tuple[OutputMapping, ...]
input_schema: Optional[Mapping[str, Any]] = None
output_schema: Optional[Mapping[str, Any]] = None
class RefreshColumnResult(_RemoteValue):
"""Terminal result of a remote Function-column refresh Job."""
rows_assigned: _UInt64
rows_failed: _UInt64
rows_remaining: _UInt64
source_version: _UInt64
published_version: Optional[_UInt64] = None
@property
def rows_filled(self) -> int:
"""Deprecated compatibility alias for :attr:`rows_assigned`."""
return self.rows_assigned
@property
def version(self) -> Optional[int]:
"""Deprecated compatibility alias for :attr:`published_version`."""
return self.published_version
_FUNCTION_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$")
_SECRET_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
def _canonical_arrow_type(data_type: pa.DataType) -> str:
primitive_types = (
(pa.bool_(), "bool"),
(pa.int8(), "int8"),
(pa.int16(), "int16"),
(pa.int32(), "int32"),
(pa.int64(), "int64"),
(pa.uint8(), "uint8"),
(pa.uint16(), "uint16"),
(pa.uint32(), "uint32"),
(pa.uint64(), "uint64"),
(pa.float16(), "float16"),
(pa.float32(), "float32"),
(pa.float64(), "float64"),
(pa.string(), "utf8"),
(pa.large_utf8(), "large_utf8"),
(pa.binary(), "binary"),
(pa.large_binary(), "large_binary"),
(pa.date32(), "date32"),
(pa.date64(), "date64"),
)
for candidate, name in primitive_types:
if data_type == candidate:
return name
if pa.types.is_fixed_size_binary(data_type):
return f"fixed_size_binary[{data_type.byte_width}]"
if pa.types.is_list(data_type):
return f"list<{_canonical_arrow_type(data_type.value_type)}>"
if pa.types.is_large_list(data_type):
return f"large_list<{_canonical_arrow_type(data_type.value_type)}>"
if pa.types.is_fixed_size_list(data_type):
return (
f"fixed_size_list<{_canonical_arrow_type(data_type.value_type)}>"
f"[{data_type.list_size}]"
)
if pa.types.is_struct(data_type):
fields = ",".join(
f"{field.name}:{_canonical_arrow_type(field.type)}" for field in data_type
)
return f"struct<{fields}>"
if pa.types.is_timestamp(data_type):
timezone = f",tz={data_type.tz}" if data_type.tz is not None else ""
return f"timestamp[{data_type.unit}{timezone}]"
if pa.types.is_time32(data_type) or pa.types.is_time64(data_type):
return f"time[{data_type.unit}]"
if pa.types.is_duration(data_type):
return f"duration[{data_type.unit}]"
if pa.types.is_decimal(data_type):
bit_width = data_type.bit_width
return f"decimal{bit_width}({data_type.precision},{data_type.scale})"
raise TypeError(f"unsupported Arrow type for Function signature: {data_type}")
def _annotation_type(annotation: Any) -> tuple[pa.DataType, bool]:
nullable = False
origin = get_origin(annotation)
if origin in (Union, types.UnionType):
arguments = get_args(annotation)
non_none = tuple(
argument for argument in arguments if argument is not type(None)
)
if len(non_none) != 1 or len(non_none) == len(arguments):
raise TypeError(f"unsupported union annotation: {annotation!r}")
annotation = non_none[0]
nullable = True
origin = get_origin(annotation)
if origin is Annotated:
base, *metadata = get_args(annotation)
arrow_types = [value for value in metadata if isinstance(value, pa.DataType)]
if len(arrow_types) != 1:
raise TypeError(
"Annotated Function types require exactly one PyArrow DataType"
)
_, base_nullable = _annotation_type(base)
return arrow_types[0], nullable or base_nullable
if isinstance(annotation, pa.DataType):
return annotation, nullable
if annotation is bool:
return pa.bool_(), nullable
if annotation is int:
return pa.int64(), nullable
if annotation is float:
return pa.float64(), nullable
if annotation is str:
return pa.string(), nullable
if annotation is bytes:
return pa.binary(), nullable
if annotation is date:
return pa.date32(), nullable
if annotation is datetime:
return pa.timestamp("us"), nullable
if get_origin(annotation) is list:
arguments = get_args(annotation)
if len(arguments) != 1:
raise TypeError(f"unsupported list annotation: {annotation!r}")
value_type, value_nullable = _annotation_type(arguments[0])
if value_nullable:
raise TypeError("nullable Function list elements are not supported")
return pa.list_(value_type), nullable
raise TypeError(f"unsupported Function annotation: {annotation!r}")
def _callable_parameters(function: Callable[..., Any]) -> tuple[inspect.Parameter, ...]:
parameters = tuple(inspect.signature(function).parameters.values())
for parameter in parameters:
if parameter.kind in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD,
):
raise TypeError("Function callables require named, non-variadic parameters")
if parameter.default is not inspect.Parameter.empty:
raise TypeError("Function callable defaults are not supported")
return parameters
def _function_output(output: pa.DataType | pa.Field | pa.Schema) -> FunctionOutput:
if isinstance(output, pa.Schema):
fields = tuple(output)
elif isinstance(output, pa.Field) and pa.types.is_struct(output.type):
if output.nullable:
raise ValueError("Function output must be non-nullable")
fields = tuple(output.type)
elif isinstance(output, pa.DataType) and pa.types.is_struct(output):
fields = tuple(output)
else:
field = (
output
if isinstance(output, pa.Field)
else pa.field("result", output, nullable=False)
)
if not isinstance(field, pa.Field):
raise TypeError(
"output_schema must be a PyArrow DataType, Field, or Schema"
)
if field.nullable:
raise ValueError("Function output must be non-nullable")
return FunctionOutput(
kind="scalar",
arrow_type=_canonical_arrow_type(field.type),
nullable=False,
)
if not fields:
raise ValueError("named-struct Function output must contain at least one field")
if any(field.nullable for field in fields):
raise ValueError("Function output fields must be non-nullable")
names = [field.name for field in fields]
if len(set(names)) != len(names):
raise ValueError("Function output field names must be unique")
return FunctionOutput(
kind="named_struct",
fields=tuple(
FunctionResultField(
name=field.name,
arrow_type=_canonical_arrow_type(field.type),
nullable=False,
)
for field in fields
),
)
def _infer_signature(
function: Callable[..., Any],
input_schema: Optional[pa.Schema],
output_schema: Optional[pa.DataType | pa.Field | pa.Schema],
) -> FunctionSignature:
parameters = _callable_parameters(function)
if (input_schema is None) != (output_schema is None):
raise ValueError("input_schema and output_schema must be provided together")
if input_schema is not None:
if not isinstance(input_schema, pa.Schema):
raise TypeError("input_schema must be a PyArrow Schema")
expected = tuple(parameter.name for parameter in parameters)
actual = tuple(input_schema.names)
if actual != expected:
raise ValueError(
"input_schema fields must exactly match callable parameters in order: "
f"expected {expected!r}, got {actual!r}"
)
inputs = tuple(
FunctionParameter(
name=field.name,
arrow_type=_canonical_arrow_type(field.type),
nullable=field.nullable,
)
for field in input_schema
)
return FunctionSignature(inputs=inputs, output=_function_output(output_schema))
try:
annotations = get_type_hints(function, include_extras=True)
except Exception as error:
raise TypeError(f"failed to resolve Function annotations: {error}") from error
missing = [
parameter.name for parameter in parameters if parameter.name not in annotations
]
if missing or "return" not in annotations:
names = missing + ([] if "return" in annotations else ["return"])
raise TypeError(f"missing Function annotations: {names!r}")
inputs = []
for parameter in parameters:
data_type, nullable = _annotation_type(annotations[parameter.name])
inputs.append(
FunctionParameter(
name=parameter.name,
arrow_type=_canonical_arrow_type(data_type),
nullable=nullable,
)
)
output_type, output_nullable = _annotation_type(annotations["return"])
if output_nullable:
raise ValueError("Function output must be non-nullable")
return FunctionSignature(
inputs=tuple(inputs),
output=_function_output(pa.field("result", output_type, nullable=False)),
)
def _is_udf_decorator(node: ast.expr) -> bool:
if isinstance(node, ast.Call):
node = node.func
return (isinstance(node, ast.Name) and node.id == "udf") or (
isinstance(node, ast.Attribute) and node.attr == "udf"
)
def _literal_source(value: Any) -> str:
if value is None or type(value) in (bool, int, str, bytes):
return repr(value)
if type(value) is float and math.isfinite(value):
return repr(value)
if type(value) is tuple:
children = ", ".join(_literal_source(child) for child in value)
if len(value) == 1:
children += ","
return f"({children})"
raise TypeError(
"Function source references an unsupported global value of type "
f"{type(value).__name__}"
)
def _package_source(function: Callable[..., Any]) -> bytes:
if not inspect.isfunction(function) or inspect.iscoroutinefunction(function):
raise TypeError("@udf requires a synchronous Python function")
try:
source = textwrap.dedent(inspect.getsource(function))
except (OSError, TypeError) as error:
raise ValueError("@udf requires inspectable Python source") from error
module = ast.parse(source)
definitions = [
node
for node in module.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == function.__name__
]
if len(definitions) != 1 or not isinstance(definitions[0], ast.FunctionDef):
raise ValueError("@udf source must contain exactly one synchronous function")
definition = definitions[0]
if any(not _is_udf_decorator(decorator) for decorator in definition.decorator_list):
raise ValueError("@udf cannot package additional Python decorators")
definition.decorator_list = []
closure = inspect.getclosurevars(function)
if closure.nonlocals:
raise ValueError("@udf cannot package functions that capture closure values")
if closure.unbound:
raise ValueError(
f"@udf source contains unresolved global names: {sorted(closure.unbound)!r}"
)
globals_source = []
for name, value in sorted(closure.globals.items()):
if isinstance(value, types.ModuleType):
globals_source.append(f"import {value.__name__} as {name}")
else:
globals_source.append(f"{name} = {_literal_source(value)}")
function_source = ast.unparse(definition)
parts = ["from __future__ import annotations"]
if globals_source:
parts.extend(["", *globals_source])
parts.extend(["", function_source, ""])
return "\n".join(parts).encode("utf-8")
class UdfDefinition:
"""A scalar Python callable prepared for remote Function registration.
Instances are created with :func:`udf`. Calling an instance executes the
original scalar Python function, which keeps local unit testing ordinary.
Remote execution adapts that scalar callable to the internal Arrow batch
ABI described by the registration artifact.
"""
def __init__(
self,
function: Callable[..., Any],
*,
name: Optional[str],
input_schema: Optional[pa.Schema],
output_schema: Optional[pa.DataType | pa.Field | pa.Schema],
pip: tuple[str, ...],
env: Mapping[str, str],
secrets: tuple[str, ...],
python_version: Optional[str],
):
function_name = name or function.__name__
if not _FUNCTION_NAME.fullmatch(function_name):
raise ValueError(f"invalid Function name: {function_name!r}")
packages = tuple(sorted(set(pip)))
if any(not package or package != package.strip() for package in packages):
raise ValueError("pip requirements must be non-empty and trimmed")
environment = dict(env)
if any(
not isinstance(key, str) or not isinstance(value, str)
for key, value in environment.items()
):
raise TypeError("Function env keys and values must be strings")
required_secrets = tuple(sorted(set(secrets)))
invalid_secrets = [
secret for secret in required_secrets if not _SECRET_NAME.fullmatch(secret)
]
if invalid_secrets:
raise ValueError(f"invalid Function secret names: {invalid_secrets!r}")
overlap = set(environment) & set(required_secrets)
if overlap:
raise ValueError(
f"Function env and secret names must be disjoint: {sorted(overlap)!r}"
)
signature = _infer_signature(function, input_schema, output_schema)
source = _package_source(function)
digest = f"sha256:{hashlib.sha256(source).hexdigest()}"
runtime = PythonRuntimeSpec(
kind="python",
python_version=python_version
or f"{sys.version_info.major}.{sys.version_info.minor}",
environment=PythonEnvironmentSpec(kind="pip", packages=packages),
env=environment,
)
self._function = function
self._request = FunctionRegistrationRequest(
name=function_name,
artifact=FunctionArtifactRequest(
kind="python_callable",
digest=digest,
entrypoint=function.__name__,
content=FunctionArtifactContent(
encoding="base64",
data=base64.b64encode(source).decode("ascii"),
),
adapter=PythonAdapterSpec(
kind="scalar_to_arrow_batch",
version=1,
),
),
signature=signature,
runtime=runtime,
required_secrets=required_secrets,
)
functools.update_wrapper(self, function)
@property
def registration_request(self) -> FunctionRegistrationRequest:
"""The immutable request sent by ``create_function_async``."""
return self._request
def __call__(self, *args, **kwargs):
return self._function(*args, **kwargs)
@overload
def udf(function: Callable[..., Any]) -> UdfDefinition: ...
@overload
def udf(
function: None = None,
*,
name: Optional[str] = None,
input_schema: Optional[pa.Schema] = None,
output_schema: Optional[pa.DataType | pa.Field | pa.Schema] = None,
pip: tuple[str, ...] | list[str] = (),
env: Optional[Mapping[str, str]] = None,
secrets: tuple[str, ...] | list[str] = (),
python_version: Optional[str] = None,
) -> Callable[[Callable[..., Any]], UdfDefinition]: ...
def udf(
function: Optional[Callable[..., Any]] = None,
*,
name: Optional[str] = None,
input_schema: Optional[pa.Schema] = None,
output_schema: Optional[pa.DataType | pa.Field | pa.Schema] = None,
pip: tuple[str, ...] | list[str] = (),
env: Optional[Mapping[str, str]] = None,
secrets: tuple[str, ...] | list[str] = (),
python_version: Optional[str] = None,
):
"""Prepare a scalar Python callable for remote Function registration.
Input and output signatures are inferred from supported annotations. For
Arrow types annotations cannot express precisely, pass ``input_schema``
and ``output_schema`` together. Nullable outputs are rejected because V1
uses physical NULL to represent unassigned computed-column rows.
Parameters
----------
function : Callable, optional
The synchronous scalar callable to package.
name : str, optional
The remote Function name. Defaults to the callable name.
input_schema : pyarrow.Schema, optional
Explicit input fields in the exact order of the callable parameters.
Must be provided together with ``output_schema``.
output_schema : pyarrow.DataType, pyarrow.Field, or pyarrow.Schema, optional
Explicit scalar or named-struct output. Must be non-nullable and be
provided together with ``input_schema``.
pip : sequence of str, optional
Pip requirements for the remote environment.
env : mapping of str to str, optional
Non-secret environment variables. Use ``secrets`` for credentials.
secrets : sequence of str, optional
Names of secrets resolved by the remote service. Secret values are not
accepted by this API or included in the registration request.
python_version : str, optional
Remote Python major/minor version. Defaults to the client version.
Returns
-------
UdfDefinition
A callable definition accepted by
:meth:`lancedb.db.DBConnection.create_function`,
:meth:`lancedb.db.AsyncConnection.create_function_async` and
:meth:`lancedb.db.DBConnection.create_function_async`.
Examples
--------
>>> from lancedb import udf
>>> @udf(pip=["numpy==2.2.0"], secrets=["MODEL_TOKEN"])
... def score(value: float) -> float:
... return value * 2
>>> score(1.5)
3.0
"""
def decorate(target: Callable[..., Any]) -> UdfDefinition:
return UdfDefinition(
target,
name=name,
input_schema=input_schema,
output_schema=output_schema,
pip=tuple(pip),
env={} if env is None else env,
secrets=tuple(secrets),
python_version=python_version,
)
if function is None:
return decorate
return decorate(function)
__all__ = [
"ApplicationInput",
"FunctionApplication",
"FunctionArtifact",
"FunctionArtifactContent",
"FunctionArtifactRequest",
"FunctionBinding",
"FunctionOutput",
"FunctionParameter",
"FunctionRegistrationRequest",
"FunctionResultField",
"FunctionSignature",
"FunctionVersion",
"FunctionVersionRef",
"InputBinding",
"OutputMapping",
"PythonEnvironmentSpec",
"PythonAdapterSpec",
"PythonRuntimeSpec",
"RefreshColumnResult",
"UdfDefinition",
"udf",
]