mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 23:02:28 +00:00
feat(functions): address Functions by namespace path (#4341)
Adds namespace addressing to Functions, matching what Secrets already have (#4150). ## API `create_function` / `create_function_async`, `get_function`, `list_functions`, `drop_function` / `drop_function_async` take `namespace_path` — keyword-only in Python (sync, async and remote connections) and a trailing `&[String]` in Rust — defaulting to the root. The namespace is addressed in the route identifier (`/v1/function/analytics$features$embed/describe`, `/v1/namespace/analytics$features/function/list`); no body carries it. `FunctionVersion` and `FunctionVersionRef` name their Function **by parts** — a short `name` plus `namespace_path` — the same shape as `SecretReference`, so a returned version reopens from its own fields: ```python fv = db.create_function(embed, namespace_path=["analytics", "features"]) fv.name, fv.namespace_path # "embed", ("analytics", "features") db.get_function(fv.name, version=fv.version, namespace_path=list(fv.namespace_path)) ``` An empty path is omitted from the wire, so root documents keep their shape. `namespace_path` is passed straight to the binding, so a bare string (`namespace_path="prod"`) is a `TypeError` rather than four one-character segments; segments are checked for addressability before a request is built. The binding and application allow-lists now share one field list, `FUNCTION_VERSION_REF_FIELDS`, which admits `namespace_path` — without it a namespaced binding or application read as coming from a newer contract. ## Service side The Sophon half serves and persists FunctionVersion / FunctionVersionRef documents in this split form, keeping the joined id as its internal catalog key. It lands in sophon with the submodule bump. ## Tests - Rust: every Function verb addresses a namespace in the path; an unaddressable segment never reaches the transport; `FunctionVersion` round-trips a namespace and `reference()` carries it; a namespaced binding and application are known shapes. - Python: sync and async verbs address the namespace; a created Function reopens from its own `name`/`namespace_path`; a bare-string path is refused before any request. Draft: compiles, `cargo fmt --check` and clippy `-D warnings` clean; lancedb lib Function tests (116) and `first_class_function_slice2` pass on this base. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0a1c1a04c7
commit
82d39485a4
@@ -149,12 +149,20 @@ class Connection(object):
|
||||
limit: Optional[int],
|
||||
) -> list[str]: ... # Deprecated: Use list_tables instead
|
||||
async def open_job(self, job_id: str) -> Job: ...
|
||||
async def create_function_async(self, request_json: str) -> Job: ...
|
||||
async def get_function(self, name: str, version: str) -> str: ...
|
||||
async def list_functions(self) -> List[str]: ...
|
||||
async def drop_function(self, name: str, version: str) -> bool: ...
|
||||
async def create_function_async(
|
||||
self, request_json: str, namespace_path: Optional[List[str]] = None
|
||||
) -> Job: ...
|
||||
async def get_function(
|
||||
self, name: str, version: str, namespace_path: Optional[List[str]] = None
|
||||
) -> str: ...
|
||||
async def list_functions(
|
||||
self, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]: ...
|
||||
async def drop_function(
|
||||
self, name: str, version: str, namespace_path: Optional[List[str]] = None
|
||||
) -> bool: ...
|
||||
async def drop_function_async(
|
||||
self, name: str, version: str
|
||||
self, name: str, version: str, namespace_path: Optional[List[str]] = None
|
||||
) -> Tuple[bool, Job]: ...
|
||||
async def create_secret(
|
||||
self, name: str, value: str, namespace_path: Optional[List[str]] = None
|
||||
|
||||
+123
-31
@@ -784,6 +784,7 @@ class DBConnection(EnforceOverrides):
|
||||
definition: UdfDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
"""Build and register a scalar Python UDF, then return its version.
|
||||
|
||||
@@ -800,6 +801,9 @@ class DBConnection(EnforceOverrides):
|
||||
Function needs, each naming a Secret and the environment variable
|
||||
its value arrives in. The Function's source is unchanged by this;
|
||||
it reads the variable the way it already did.
|
||||
namespace_path : list of str, optional
|
||||
The namespace holding the Function. None or an empty list is the
|
||||
root namespace.
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -815,13 +819,16 @@ class DBConnection(EnforceOverrides):
|
||||
)
|
||||
```
|
||||
"""
|
||||
return self.create_function_async(definition, secrets=secrets).wait()
|
||||
return self.create_function_async(
|
||||
definition, secrets=secrets, namespace_path=namespace_path
|
||||
).wait()
|
||||
|
||||
def create_function_async(
|
||||
self,
|
||||
definition: UdfDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Job[FunctionVersion]:
|
||||
"""Submit a scalar Python UDF for building and registration.
|
||||
|
||||
@@ -833,17 +840,26 @@ class DBConnection(EnforceOverrides):
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
||||
def get_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
"""Open one exact immutable Function version from the remote catalog."""
|
||||
raise NotImplementedError(
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def list_functions(self) -> List[FunctionVersion]:
|
||||
"""List every published immutable Function version.
|
||||
def list_functions(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[FunctionVersion]:
|
||||
"""List every published immutable Function version in a namespace.
|
||||
|
||||
Results are ordered by Function name then version. Local connections
|
||||
raise ``NotImplementedError``.
|
||||
Functions in child namespaces are not included. Results are ordered by
|
||||
Function name then version. Local connections raise
|
||||
``NotImplementedError``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -857,7 +873,13 @@ class DBConnection(EnforceOverrides):
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def drop_function(self, name: str, *, version: str) -> bool:
|
||||
def drop_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> bool:
|
||||
"""Drop a Function name and the object it was bound to.
|
||||
|
||||
The requested version must exist in the currently named object. Returns
|
||||
@@ -870,7 +892,13 @@ class DBConnection(EnforceOverrides):
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def drop_function_async(self, name: str, *, version: str) -> Tuple[bool, Job]:
|
||||
def drop_function_async(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, Job]:
|
||||
"""Drop a Function name and return its cleanup job.
|
||||
|
||||
The name is unbound before this returns; the object's content may still
|
||||
@@ -1808,25 +1836,62 @@ class LanceDBConnection(DBConnection):
|
||||
definition: UdfDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Job[FunctionVersion]:
|
||||
job = LOOP.run(self._conn.create_function_async(definition, secrets=secrets))
|
||||
job = LOOP.run(
|
||||
self._conn.create_function_async(
|
||||
definition, secrets=secrets, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
return Job(job)
|
||||
|
||||
@override
|
||||
def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
||||
return LOOP.run(self._conn.get_function(name, version=version))
|
||||
def get_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
return LOOP.run(
|
||||
self._conn.get_function(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def list_functions(self) -> List[FunctionVersion]:
|
||||
return LOOP.run(self._conn.list_functions())
|
||||
def list_functions(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[FunctionVersion]:
|
||||
return LOOP.run(self._conn.list_functions(namespace_path=namespace_path))
|
||||
|
||||
@override
|
||||
def drop_function(self, name: str, *, version: str) -> bool:
|
||||
return LOOP.run(self._conn.drop_function(name, version=version))
|
||||
def drop_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> bool:
|
||||
return LOOP.run(
|
||||
self._conn.drop_function(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def drop_function_async(self, name: str, *, version: str) -> Tuple[bool, Job]:
|
||||
dropped, job = LOOP.run(self._conn.drop_function_async(name, version=version))
|
||||
def drop_function_async(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, Job]:
|
||||
dropped, job = LOOP.run(
|
||||
self._conn.drop_function_async(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
return dropped, Job(job)
|
||||
|
||||
@override
|
||||
@@ -2787,6 +2852,7 @@ class AsyncConnection(object):
|
||||
definition: UdfDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> AsyncJob[FunctionVersion]:
|
||||
"""Submit a scalar Python UDF for building and registration.
|
||||
|
||||
@@ -2794,36 +2860,60 @@ class AsyncConnection(object):
|
||||
artifact. Waiting on the job returns the immutable Function version.
|
||||
``secrets`` is a sequence of
|
||||
[EnvVarSecret][lancedb.secrets.EnvVarSecret], each naming a Secret and
|
||||
the environment variable its value arrives in. Local connections raise
|
||||
``NotImplementedError``.
|
||||
the environment variable its value arrives in. ``namespace_path`` is
|
||||
the namespace the Function is registered in; None or an empty list is
|
||||
the root. Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
if not isinstance(definition, UdfDefinition):
|
||||
raise TypeError("create_function_async requires a @udf definition")
|
||||
request = definition.bind_secrets(secrets)
|
||||
inner = await self._inner.create_function_async(request.to_canonical_json())
|
||||
inner = await self._inner.create_function_async(
|
||||
request.to_canonical_json(), namespace_path
|
||||
)
|
||||
return _typed_job(inner, FunctionVersion.from_json)
|
||||
|
||||
async def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
||||
async def get_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
"""Open one exact immutable Function version from the remote catalog."""
|
||||
return FunctionVersion.from_json(await self._inner.get_function(name, version))
|
||||
return FunctionVersion.from_json(
|
||||
await self._inner.get_function(name, version, namespace_path)
|
||||
)
|
||||
|
||||
async def list_functions(self) -> List[FunctionVersion]:
|
||||
"""List every published immutable Function version.
|
||||
async def list_functions(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[FunctionVersion]:
|
||||
"""List every published immutable Function version in a namespace.
|
||||
|
||||
Results are ordered by Function name then version. Local connections
|
||||
raise ``NotImplementedError``.
|
||||
Functions in child namespaces are not included. Results are ordered by
|
||||
Function name then version. Local connections raise
|
||||
``NotImplementedError``.
|
||||
"""
|
||||
return [
|
||||
FunctionVersion.from_json(value)
|
||||
for value in await self._inner.list_functions()
|
||||
for value in await self._inner.list_functions(namespace_path)
|
||||
]
|
||||
|
||||
async def drop_function(self, name: str, *, version: str) -> bool:
|
||||
async def drop_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> bool:
|
||||
"""Drop a Function name and the object it was bound to."""
|
||||
return await self._inner.drop_function(name, version)
|
||||
return await self._inner.drop_function(name, version, namespace_path)
|
||||
|
||||
async def drop_function_async(
|
||||
self, name: str, *, version: str
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, AsyncJob]:
|
||||
"""Drop a Function name and return its cleanup job.
|
||||
|
||||
@@ -2831,7 +2921,9 @@ class AsyncConnection(object):
|
||||
be being deleted. Await :meth:`AsyncJob.wait` to wait for that to
|
||||
finish.
|
||||
"""
|
||||
dropped, job = await self._inner.drop_function_async(name, version)
|
||||
dropped, job = await self._inner.drop_function_async(
|
||||
name, version, namespace_path
|
||||
)
|
||||
return dropped, AsyncJob(job)
|
||||
|
||||
async def create_secret(
|
||||
|
||||
@@ -356,9 +356,16 @@ _ObjectVersion = Annotated[
|
||||
|
||||
|
||||
class FunctionVersion(_RemoteValue):
|
||||
"""A pinned object revision, independent of its executable image digest."""
|
||||
"""A pinned object revision, independent of its executable image digest.
|
||||
|
||||
``name`` and ``namespace_path`` are the Function's parts, as a
|
||||
[SecretReference][lancedb.functions.SecretReference] carries a Secret's;
|
||||
they are what [get_function][lancedb.db.DBConnection.get_function] takes.
|
||||
An empty ``namespace_path`` is the root namespace.
|
||||
"""
|
||||
|
||||
name: str
|
||||
namespace_path: tuple[str, ...] = ()
|
||||
object_id: str
|
||||
location: str
|
||||
version: _ObjectVersion
|
||||
@@ -444,6 +451,7 @@ class FunctionVersion(_RemoteValue):
|
||||
return FunctionApplication(
|
||||
function=FunctionVersionRef(
|
||||
name=self.name,
|
||||
namespace_path=self.namespace_path,
|
||||
object_id=self.object_id,
|
||||
location=self.location,
|
||||
version=self.version,
|
||||
@@ -502,6 +510,7 @@ class FunctionRegistrationRequest(_RemoteValue):
|
||||
|
||||
class FunctionVersionRef(_OpenRemoteValue):
|
||||
name: str
|
||||
namespace_path: tuple[str, ...] = ()
|
||||
object_id: str
|
||||
location: str
|
||||
version: _ObjectVersion
|
||||
|
||||
@@ -862,25 +862,62 @@ class RemoteDBConnection(DBConnection):
|
||||
definition: UdfDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Job[FunctionVersion]:
|
||||
job = LOOP.run(self._conn.create_function_async(definition, secrets=secrets))
|
||||
job = LOOP.run(
|
||||
self._conn.create_function_async(
|
||||
definition, secrets=secrets, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
return Job(job)
|
||||
|
||||
@override
|
||||
def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
||||
return LOOP.run(self._conn.get_function(name, version=version))
|
||||
def get_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
return LOOP.run(
|
||||
self._conn.get_function(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def list_functions(self) -> List[FunctionVersion]:
|
||||
return LOOP.run(self._conn.list_functions())
|
||||
def list_functions(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[FunctionVersion]:
|
||||
return LOOP.run(self._conn.list_functions(namespace_path=namespace_path))
|
||||
|
||||
@override
|
||||
def drop_function(self, name: str, *, version: str) -> bool:
|
||||
return LOOP.run(self._conn.drop_function(name, version=version))
|
||||
def drop_function(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> bool:
|
||||
return LOOP.run(
|
||||
self._conn.drop_function(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def drop_function_async(self, name: str, *, version: str) -> Tuple[bool, Job]:
|
||||
dropped, job = LOOP.run(self._conn.drop_function_async(name, version=version))
|
||||
def drop_function_async(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, Job]:
|
||||
dropped, job = LOOP.run(
|
||||
self._conn.drop_function_async(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
return dropped, Job(job)
|
||||
|
||||
@override
|
||||
|
||||
@@ -162,6 +162,29 @@ def test_function_version_binds_named_columns_as_one_immutable_application():
|
||||
] == [("text", "column", "documents.body")]
|
||||
|
||||
|
||||
def test_function_version_names_its_namespace_as_a_separate_field():
|
||||
"""A Function is named by its parts, as a Secret is; a root one states no
|
||||
namespace."""
|
||||
value = job_result("remote_function_job.json")
|
||||
root = FunctionVersion.from_json(json.dumps(value))
|
||||
assert root.namespace_path == ()
|
||||
assert "namespace_path" not in json.loads(root.to_canonical_json())
|
||||
|
||||
nested = FunctionVersion(**{**value, "namespace_path": ["analytics", "features"]})
|
||||
assert nested.name == "embed"
|
||||
assert nested.namespace_path == ("analytics", "features")
|
||||
assert json.loads(nested.to_canonical_json())["namespace_path"] == [
|
||||
"analytics",
|
||||
"features",
|
||||
]
|
||||
application = nested(text=col("documents.body"))
|
||||
assert application.function.name == "embed"
|
||||
assert application.function.namespace_path == ("analytics", "features")
|
||||
assert json.loads(application.to_canonical_json())["function"][
|
||||
"namespace_path"
|
||||
] == ["analytics", "features"]
|
||||
|
||||
|
||||
def test_function_version_binding_validates_names_and_direct_columns():
|
||||
version = FunctionVersion.from_json(
|
||||
json.dumps(job_result("remote_function_job.json"))
|
||||
|
||||
@@ -1539,8 +1539,10 @@ def _mock_remote_function_catalog():
|
||||
else (None, None)
|
||||
)
|
||||
if function_action[1] == "create":
|
||||
*namespace_path, name = function_action[0].split("$")
|
||||
state["version"] = {
|
||||
"name": "normalize_score",
|
||||
"name": name,
|
||||
**({"namespace_path": namespace_path} if namespace_path else {}),
|
||||
"version": FUNCTION_VERSION,
|
||||
"object_id": "fixture",
|
||||
"location": "memory:///fixture",
|
||||
@@ -1565,10 +1567,18 @@ def _mock_remote_function_catalog():
|
||||
"job_state": "DONE",
|
||||
"result": state["version"],
|
||||
}
|
||||
elif self.path == "/v1/function/normalize_score/describe":
|
||||
# A namespaced Function's id is its namespace path and name joined
|
||||
# by `$`, so the name is the last segment of it.
|
||||
elif (
|
||||
function_action[1] == "describe"
|
||||
and function_action[0].split("$")[-1] == "normalize_score"
|
||||
):
|
||||
assert body == {"version": FUNCTION_VERSION}
|
||||
response = state["version"]
|
||||
elif self.path == "/v1/function/normalize_score/drop":
|
||||
elif (
|
||||
function_action[1] == "drop"
|
||||
and function_action[0].split("$")[-1] == "normalize_score"
|
||||
):
|
||||
assert body == {"version": FUNCTION_VERSION}
|
||||
response = {"dropped": True}
|
||||
elif secret_action[1] in ("create", "alter"):
|
||||
@@ -1602,7 +1612,8 @@ def _mock_remote_function_catalog():
|
||||
assert query["page_token"] == "next"
|
||||
self._write_response(200, {"secrets": [{"name": "hf-prod"}]})
|
||||
return
|
||||
if url.path != "/v1/namespace/$/function/list":
|
||||
parts = url.path.strip("/").split("/")
|
||||
if parts[:2] != ["v1", "namespace"] or parts[3:] != ["function", "list"]:
|
||||
self._write_response(404, {"error": "not found"})
|
||||
return
|
||||
assert query["include_definition"] == "true"
|
||||
@@ -1840,3 +1851,102 @@ async def test_async_remote_drop_function_sends_exact_version():
|
||||
{"version": FUNCTION_VERSION},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_remote_function_verbs_address_a_namespace_in_the_path():
|
||||
"""A Function's namespace is part of the identifier each route addresses;
|
||||
no body carries it, so a namespaced request differs only in its path."""
|
||||
namespace = ["analytics", "features"]
|
||||
with _mock_remote_function_catalog() as (host, state):
|
||||
db = lancedb.connect(
|
||||
"db://dev",
|
||||
api_key="fake",
|
||||
host_override=host,
|
||||
client_config={"retry_config": {"retries": 0}},
|
||||
)
|
||||
created = db.create_function(normalize_score, namespace_path=namespace)
|
||||
# The returned parts are what the other verbs take, so a created
|
||||
# Function reopens from its own fields.
|
||||
assert created.name == "normalize_score"
|
||||
assert created.namespace_path == tuple(namespace)
|
||||
reopened = db.get_function(
|
||||
created.name,
|
||||
version=created.version,
|
||||
namespace_path=list(created.namespace_path),
|
||||
)
|
||||
assert reopened == created
|
||||
assert reopened(value=lancedb.col("score")).function.namespace_path == tuple(
|
||||
namespace
|
||||
)
|
||||
db.list_functions(namespace_path=namespace)
|
||||
assert (
|
||||
db.drop_function(
|
||||
"normalize_score", version=FUNCTION_VERSION, namespace_path=namespace
|
||||
)
|
||||
is True
|
||||
)
|
||||
dropped, _ = db.drop_function_async(
|
||||
"normalize_score", version=FUNCTION_VERSION, namespace_path=namespace
|
||||
)
|
||||
assert dropped is True
|
||||
|
||||
assert [path for path, _ in state["requests"]] == [
|
||||
"/v1/function/analytics$features$normalize_score/create",
|
||||
"/v1/jobs/describe",
|
||||
"/v1/function/analytics$features$normalize_score/describe",
|
||||
"/v1/namespace/analytics$features/function/list",
|
||||
"/v1/namespace/analytics$features/function/list",
|
||||
"/v1/function/analytics$features$normalize_score/drop",
|
||||
"/v1/function/analytics$features$normalize_score/drop",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_remote_function_verbs_address_a_namespace_in_the_path():
|
||||
namespace = ["analytics", "features"]
|
||||
with _mock_remote_function_catalog() as (host, state):
|
||||
db = await lancedb.connect_async(
|
||||
"db://dev",
|
||||
api_key="fake",
|
||||
host_override=host,
|
||||
client_config={"retry_config": {"retries": 0}},
|
||||
)
|
||||
registration = await db.create_function_async(
|
||||
normalize_score, namespace_path=namespace
|
||||
)
|
||||
await registration.wait()
|
||||
await db.get_function(
|
||||
"normalize_score", version=FUNCTION_VERSION, namespace_path=namespace
|
||||
)
|
||||
await db.list_functions(namespace_path=namespace)
|
||||
assert (
|
||||
await db.drop_function(
|
||||
"normalize_score", version=FUNCTION_VERSION, namespace_path=namespace
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
assert [path for path, _ in state["requests"]] == [
|
||||
"/v1/function/analytics$features$normalize_score/create",
|
||||
"/v1/jobs/describe",
|
||||
"/v1/function/analytics$features$normalize_score/describe",
|
||||
"/v1/namespace/analytics$features/function/list",
|
||||
"/v1/namespace/analytics$features/function/list",
|
||||
"/v1/function/analytics$features$normalize_score/drop",
|
||||
]
|
||||
|
||||
|
||||
def test_a_function_namespace_path_must_be_a_list_of_segments():
|
||||
"""A bare string is refused rather than read as a sequence of one-character
|
||||
segments, and nothing reaches the service."""
|
||||
with _mock_remote_function_catalog() as (host, state):
|
||||
db = lancedb.connect(
|
||||
"db://dev",
|
||||
api_key="fake",
|
||||
host_override=host,
|
||||
client_config={"retry_config": {"retries": 0}},
|
||||
)
|
||||
with pytest.raises(TypeError):
|
||||
db.list_functions(namespace_path="analytics")
|
||||
|
||||
assert state["requests"] == []
|
||||
|
||||
@@ -754,31 +754,37 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (request_json, namespace_path=None))]
|
||||
pub fn create_function_async(
|
||||
self_: PyRef<'_, Self>,
|
||||
request_json: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let request = lancedb::function::FunctionRegistrationRequest::from_json(&request_json)
|
||||
.infer_error()?;
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.create_function_async(request)
|
||||
.create_function_async(request, &namespace_path)
|
||||
.await
|
||||
.infer_error()
|
||||
.map(crate::job::Job::new_typed)
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, version, namespace_path=None))]
|
||||
pub fn get_function(
|
||||
self_: PyRef<'_, Self>,
|
||||
name: String,
|
||||
version: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.get_function(name, version)
|
||||
.get_function(name, version, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_canonical_json()
|
||||
@@ -786,11 +792,16 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_functions(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
||||
#[pyo3(signature = (namespace_path=None))]
|
||||
pub fn list_functions(
|
||||
self_: PyRef<'_, Self>,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.list_functions()
|
||||
.list_functions(&namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.into_iter()
|
||||
@@ -799,26 +810,35 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, version, namespace_path=None))]
|
||||
pub fn drop_function(
|
||||
self_: PyRef<'_, Self>,
|
||||
name: String,
|
||||
version: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner.drop_function(name, version).await.infer_error()
|
||||
inner
|
||||
.drop_function(name, version, &namespace_path)
|
||||
.await
|
||||
.infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, version, namespace_path=None))]
|
||||
pub fn drop_function_async(
|
||||
self_: PyRef<'_, Self>,
|
||||
name: String,
|
||||
version: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.drop_function_async(name, version)
|
||||
.drop_function_async(name, version, &namespace_path)
|
||||
.await
|
||||
.infer_error()
|
||||
.map(|(dropped, job)| (dropped, crate::job::Job::new(job)))
|
||||
|
||||
Reference in New Issue
Block a user