mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
feat(python): support exist_ok when creating secrets (#4978)
Add `exist_ok=False` to synchronous and asynchronous Python
`create_secret`. Passing `True` uses `CREATE SECRET IF NOT EXISTS`,
preserving an existing credential. The default continues to fail on an
existing name; rotation stays explicit through `alter_secret`.
```python
db.create_secret("api-key", value, exist_ok=True)
await async_db.create_secret("api-key", value, exist_ok=True)
```
The Rust API gains an awaitable builder with `.exist_ok(true)` while
existing `create_secret(...).await` calls keep working. Includes Python
SQL round-trip coverage and Rust coverage for direct and deferred SQL
responses.
This commit is contained in:
@@ -228,7 +228,12 @@ class Connection(object):
|
||||
self, name: str, namespace_path: Optional[List[str]] = None
|
||||
) -> None: ...
|
||||
async def create_secret(
|
||||
self, name: str, value: str, namespace_path: Optional[List[str]] = None
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
*,
|
||||
exist_ok: bool = False,
|
||||
) -> None: ...
|
||||
async def alter_secret(
|
||||
self, name: str, value: str, namespace_path: Optional[List[str]] = None
|
||||
|
||||
@@ -1158,14 +1158,24 @@ class DBConnection(EnforceOverrides):
|
||||
)
|
||||
|
||||
def create_secret(
|
||||
self, name: str, value: str, *, namespace_path: Optional[List[str]] = None
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
*,
|
||||
exist_ok: bool = False,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
"""Create a named Secret in this database.
|
||||
|
||||
Fails if the name is taken, so a create never silently becomes a
|
||||
rotation. Nothing reads the value back: it is bound to a Function by
|
||||
Fails if the name is taken unless ``exist_ok=True``, which leaves the
|
||||
existing value unchanged. Use :meth:`alter_secret` to rotate a Secret.
|
||||
Nothing reads the value back: it is bound to a Function by
|
||||
name and resolved by the service when that Function runs. Local
|
||||
connections raise ``NotImplementedError``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> db.create_secret("api-key", "credential", exist_ok=True) # doctest: +SKIP
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
_append_doc_url(
|
||||
@@ -2492,9 +2502,18 @@ class LanceDBConnection(DBConnection):
|
||||
|
||||
@override
|
||||
def create_secret(
|
||||
self, name: str, value: str, *, namespace_path: Optional[List[str]] = None
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
*,
|
||||
exist_ok: bool = False,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
LOOP.run(self._conn.create_secret(name, value, namespace_path=namespace_path))
|
||||
LOOP.run(
|
||||
self._conn.create_secret(
|
||||
name, value, exist_ok=exist_ok, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def alter_secret(
|
||||
@@ -3949,17 +3968,30 @@ class AsyncConnection(object):
|
||||
)
|
||||
|
||||
async def create_secret(
|
||||
self, name: str, value: str, *, namespace_path: Optional[List[str]] = None
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
*,
|
||||
exist_ok: bool = False,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
"""Create a named Secret in this database.
|
||||
|
||||
Fails if the name is taken, so a create never silently becomes a
|
||||
rotation. Nothing reads the value back.
|
||||
Fails if the name is taken unless ``exist_ok=True``, which leaves the
|
||||
existing value unchanged. Use :meth:`alter_secret` to rotate a Secret.
|
||||
Nothing reads the value back.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> await db.create_secret( # doctest: +SKIP
|
||||
... "api-key", "credential", exist_ok=True
|
||||
... )
|
||||
"""
|
||||
await self._inner.create_secret(
|
||||
validate_secret_name(name),
|
||||
value,
|
||||
list(validate_namespace_path(namespace_path)),
|
||||
exist_ok=exist_ok,
|
||||
)
|
||||
|
||||
async def alter_secret(
|
||||
|
||||
@@ -1129,9 +1129,18 @@ class RemoteDBConnection(DBConnection):
|
||||
|
||||
@override
|
||||
def create_secret(
|
||||
self, name: str, value: str, *, namespace_path: Optional[List[str]] = None
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
*,
|
||||
exist_ok: bool = False,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
LOOP.run(self._conn.create_secret(name, value, namespace_path=namespace_path))
|
||||
LOOP.run(
|
||||
self._conn.create_secret(
|
||||
name, value, exist_ok=exist_ok, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def alter_secret(
|
||||
|
||||
@@ -293,6 +293,56 @@ def test_parameters_reach_the_server_as_the_values_that_were_sent():
|
||||
assert echoed.column("k").to_pylist() == [10]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("asynchronous", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("exist_ok", "status"),
|
||||
[
|
||||
(None, "created"),
|
||||
(False, "created"),
|
||||
(True, "created"),
|
||||
(True, "already_exists"),
|
||||
],
|
||||
)
|
||||
async def test_create_secret_exist_ok_reaches_sql(asynchronous, exist_ok, status):
|
||||
flight = pytest.importorskip("pyarrow.flight")
|
||||
|
||||
class SecretServer(flight.FlightServerBase):
|
||||
def __init__(self):
|
||||
super().__init__("grpc://127.0.0.1:0")
|
||||
self.writes = []
|
||||
|
||||
def do_exchange(self, context, descriptor, reader, writer):
|
||||
self.writes.append((descriptor.command, reader.read_all()))
|
||||
result = pa.table({"status": [status]})
|
||||
writer.begin(result.schema)
|
||||
writer.write_table(result)
|
||||
writer.close()
|
||||
|
||||
with SecretServer() as server:
|
||||
options = {
|
||||
"api_key": "test-key",
|
||||
"host_override": "http://localhost:10024",
|
||||
"sql_host_override": f"grpc://127.0.0.1:{server.port}",
|
||||
}
|
||||
kwargs = {"namespace_path": ["team", "production"]}
|
||||
if exist_ok is not None:
|
||||
kwargs["exist_ok"] = exist_ok
|
||||
if asynchronous:
|
||||
db = await lancedb.connect_async("db://analytics", **options)
|
||||
await db.create_secret("api-key", "credential's value", **kwargs)
|
||||
else:
|
||||
db = lancedb.connect("db://analytics", **options)
|
||||
db.create_secret("api-key", "credential's value", **kwargs)
|
||||
((command, parameters),) = server.writes
|
||||
|
||||
prefix = "CREATE SECRET IF NOT EXISTS" if exist_ok else "CREATE SECRET"
|
||||
sql = f'{prefix} "api-key" TYPE = GENERIC_STRING SECRET_STRING = $1'
|
||||
assert sql.encode() in command
|
||||
assert b"credential's value" not in command
|
||||
assert parameters.column(0).to_pylist() == ["credential's value"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("job_id", [None, "sql-job-1"])
|
||||
def test_sql_input_error_metadata_crosses_native_boundary(job_id):
|
||||
flight = pytest.importorskip("pyarrow.flight")
|
||||
|
||||
@@ -1036,18 +1036,20 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, value, namespace_path=None))]
|
||||
#[pyo3(signature = (name, value, namespace_path=None, *, exist_ok=false))]
|
||||
pub fn create_secret(
|
||||
self_: PyRef<'_, Self>,
|
||||
name: String,
|
||||
value: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
exist_ok: bool,
|
||||
) -> 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
|
||||
.create_secret(name, value, &namespace_path)
|
||||
.exist_ok(exist_ok)
|
||||
.await
|
||||
.infer_error()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user