feat: add database storage integration APIs (#4618)

Adds database-owned S3 storage integrations with Secret-style namespace
paths in a separate keyspace. Use a regular database under the cluster
root for bootstrap integrations and Secrets. `default` is an example
name, with no enforced or automatically provisioned database; `system`
remains read-only. Authentication uses either an IAM role or two
existing Secret references, including cross-database references;
configuration metadata never contains credential values.

### Python experience

```python
catalog = lancedb.connect_catalog("https://my-server.example", api_key="secret")
db = catalog.connect_database("default")
info = db.create_storage_integration(
    "raw", "s3://customer-bucket/raw/",
    aws_role_arn="arn:aws:iam::123456789012:role/lancedb-access",
    aws_region="us-east-1",
)
print(info.configuration_instructions)
info = db.describe_storage_integration("raw")
print(list(db.list_storage_integrations()))
db.drop_storage_integration("raw")
```

For static credentials, replace `aws_role_arn` with
`aws_access_key_id_secret=SecretReference("s3_key_id",
database="default")` and
`aws_secret_access_key_secret=SecretReference("s3_secret_key",
database="default")`, imported from `lancedb.storage_integration`. Pass
`namespace_path` to address nested namespaces. Async database
connections expose the same APIs; listing is lazy. All four client
operations execute SQL through the existing SQL transport, with typed
Arrow result decoding and name-based listing cursors.

### SQL experience

```sql
CREATE STORAGE INTEGRATION default.public.raw
    STORAGE_PROVIDER = 'S3'
    STORAGE_PREFIX = 's3://customer-bucket/raw/'
    STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/lancedb-access'
    STORAGE_AWS_REGION = 'us-east-1';
DESCRIBE STORAGE INTEGRATION default.public.raw;
SHOW STORAGE INTEGRATIONS IN default.public;
DROP STORAGE INTEGRATION IF EXISTS default.public.raw;

CREATE STORAGE INTEGRATION default.public.raw
    STORAGE_PROVIDER = 'S3'
    STORAGE_PREFIX = 's3://customer-bucket/raw/'
    STORAGE_AWS_ACCESS_KEY_ID_SECRET = default.public.s3_key_id
    STORAGE_AWS_SECRET_ACCESS_KEY_SECRET = default.public.s3_secret_key;
```

CREATE and DESCRIBE return configuration and a single sentence of setup
guidance. IAM mode also returns the deployment role principal, stable
external ID, customer trust policy, and prefix-scoped permissions
policy. Registration does not verify access. Creation requires namespace
CREATE_STORAGE_INTEGRATION plus parent USAGE; Secret references require
source database, namespace, and Secret USAGE. Describe requires object
USAGE; drop requires OWNERSHIP. SQL supports GRANT/REVOKE for these
objects and their creation privilege.

Includes client documentation and Flight SQL regression coverage; no
dedicated storage-integration REST endpoints are used. Local
builds/tests are excluded by request.
This commit is contained in:
Jack Ye
2026-10-30 00:26:42 -07:00
committed by GitHub
parent a39a31807f
commit 6d126a3601
24 changed files with 1453 additions and 11 deletions
+1
View File
@@ -41,6 +41,7 @@ from .functions import (
)
from .secrets import EnvVarSecret as EnvVarSecret
from .secrets import SecretInfo as SecretInfo
from .storage_integration import StorageIntegrationInfo as StorageIntegrationInfo
from .materialized_view import (
AsyncMaterializedView,
MaterializedView,
+20
View File
@@ -171,6 +171,22 @@ class Connection(object):
async def drop_function_async(
self, name: str, version: str, namespace_path: Optional[List[str]] = None
) -> Tuple[bool, Job]: ...
async def create_storage_integration(
self, name: str, request: str, namespace_path: Optional[List[str]] = None
) -> str: ...
async def describe_storage_integration(
self, name: str, namespace_path: Optional[List[str]] = None
) -> str: ...
def list_storage_integrations(
self,
*,
namespace_path: Optional[List[str]] = None,
page_token: Optional[str] = None,
page_limit: Optional[int] = None,
) -> NameListing: ...
async def drop_storage_integration(
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
) -> None: ...
@@ -1080,6 +1096,10 @@ class AuthzObject:
database: str, namespace_path: Sequence[str], name: str
) -> AuthzObject: ...
@staticmethod
def storage_integration(
database: str, namespace: Sequence[str], name: str
) -> AuthzObject: ...
@staticmethod
def secret(
database: str, namespace_path: Sequence[str], name: str
) -> AuthzObject: ...
+12
View File
@@ -172,6 +172,17 @@ class Object:
"""A secret, defaulting to the database's public namespace."""
return cls(str(_lancedb.AuthzObject.secret(database, namespace_path, name)))
@classmethod
def storage_integration(
cls, *, database: str, name: str, namespace_path: Sequence[str] = ("public",)
) -> "Object":
"""A database storage integration, defaulting to its public namespace."""
return cls(
str(
_lancedb.AuthzObject.storage_integration(database, namespace_path, name)
)
)
@classmethod
def function(
cls, *, database: str, name: str, namespace_path: Sequence[str] = ("public",)
@@ -217,6 +228,7 @@ class Privilege(str, Enum):
CREATE_SECRET = "CREATE_SECRET"
CREATE_FUNCTION = "CREATE_FUNCTION"
OPERATE = "OPERATE"
CREATE_STORAGE_INTEGRATION = "CREATE_STORAGE_INTEGRATION"
@dataclass(frozen=True)
+1 -1
View File
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Remote catalogs manage databases through a server's root namespace."""
"""Remote catalogs manage databases."""
from functools import cached_property
from datetime import timedelta
+187
View File
@@ -8,6 +8,8 @@ from abc import abstractmethod
import asyncio
from datetime import timedelta
from pathlib import Path
from dataclasses import asdict
import json
import sys
from typing import (
TYPE_CHECKING,
@@ -58,6 +60,7 @@ from . import __version__
from ._lancedb import connect as lancedb_connect # type: ignore
from .functions import FunctionVersion, UdfDefinition
from .job import AsyncJob, Job, _typed_job
from .storage_integration import SecretReference, StorageIntegrationInfo
from .sql import AsyncQuery as AsyncSqlQuery
from .sql import Query as SqlQuery
from .sql import QueryDescription, QueryParameters, to_parameter_batch
@@ -375,6 +378,38 @@ class DBConnection(EnforceOverrides):
"""
pass
def create_storage_integration(
self,
name: str,
storage_prefix: str,
*,
aws_role_arn: Optional[str] = None,
aws_access_key_id_secret: Optional[Union[str, SecretReference]] = None,
aws_secret_access_key_secret: Optional[Union[str, SecretReference]] = None,
aws_region: Optional[str] = None,
namespace_path: Optional[List[str]] = None,
) -> StorageIntegrationInfo:
raise NotImplementedError
def describe_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> StorageIntegrationInfo:
raise NotImplementedError
def list_storage_integrations(
self,
*,
namespace_path: Optional[List[str]] = None,
page_token: Optional[str] = None,
page_limit: Optional[int] = None,
) -> Listing[str]:
raise NotImplementedError
def drop_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> None:
raise NotImplementedError
@abstractmethod
def create_table(
self,
@@ -2099,6 +2134,62 @@ class LanceDBConnection(DBConnection):
)
return dropped, Job(job)
@override
def create_storage_integration(
self,
name: str,
storage_prefix: str,
*,
aws_role_arn: Optional[str] = None,
aws_access_key_id_secret: Optional[Union[str, SecretReference]] = None,
aws_secret_access_key_secret: Optional[Union[str, SecretReference]] = None,
aws_region: Optional[str] = None,
namespace_path: Optional[List[str]] = None,
) -> StorageIntegrationInfo:
return LOOP.run(
self._conn.create_storage_integration(
name,
storage_prefix,
aws_role_arn=aws_role_arn,
aws_access_key_id_secret=aws_access_key_id_secret,
aws_secret_access_key_secret=aws_secret_access_key_secret,
aws_region=aws_region,
namespace_path=namespace_path,
)
)
@override
def describe_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> StorageIntegrationInfo:
return LOOP.run(
self._conn.describe_storage_integration(name, namespace_path=namespace_path)
)
@override
def list_storage_integrations(
self,
*,
namespace_path: Optional[List[str]] = None,
page_token: Optional[str] = None,
page_limit: Optional[int] = None,
) -> Listing[str]:
return Listing(
self._conn.list_storage_integrations(
namespace_path=namespace_path,
page_token=page_token,
page_limit=page_limit,
)
)
@override
def drop_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> None:
LOOP.run(
self._conn.drop_storage_integration(name, namespace_path=namespace_path)
)
@override
def create_secret(
self, name: str, value: str, *, namespace_path: Optional[List[str]] = None
@@ -3267,6 +3358,102 @@ class AsyncConnection(object):
)
return dropped, AsyncJob(job)
async def create_storage_integration(
self,
name: str,
storage_prefix: str,
*,
aws_role_arn: Optional[str] = None,
aws_access_key_id_secret: Optional[Union[str, SecretReference]] = None,
aws_secret_access_key_secret: Optional[Union[str, SecretReference]] = None,
aws_region: Optional[str] = None,
namespace_path: Optional[List[str]] = None,
) -> StorageIntegrationInfo:
"""Register database-owned S3 access using a role or two Secret references.
``namespace_path`` follows Secret naming rules. A SecretReference can name
another database; a string names a root Secret in this database.
Registration returns setup instructions without verifying storage access.
"""
role_mode = aws_role_arn is not None
key_mode = (
aws_access_key_id_secret is not None
and aws_secret_access_key_secret is not None
)
if role_mode == key_mode or (
role_mode
and (
aws_access_key_id_secret is not None
or aws_secret_access_key_secret is not None
)
):
raise ValueError(
"Specify either aws_role_arn or both AWS credential Secret references"
)
def reference(value):
if value is None:
return None
if isinstance(value, str):
value = SecretReference(value)
if not isinstance(value, SecretReference):
raise TypeError(
"Credential secrets must be names or SecretReference objects"
)
return asdict(value)
request = {
"storage_provider": "S3",
"storage_prefix": storage_prefix,
"aws_role_arn": aws_role_arn,
"aws_region": aws_region,
"aws_access_key_id_secret": reference(aws_access_key_id_secret),
"aws_secret_access_key_secret": reference(aws_secret_access_key_secret),
}
info = await self._inner.create_storage_integration(
validate_secret_name(name),
json.dumps(request),
list(validate_namespace_path(namespace_path)),
)
return StorageIntegrationInfo(**json.loads(info))
async def describe_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> StorageIntegrationInfo:
"""Return configuration and Secret references without credential values."""
return StorageIntegrationInfo(
**json.loads(
await self._inner.describe_storage_integration(
validate_secret_name(name),
list(validate_namespace_path(namespace_path)),
)
)
)
def list_storage_integrations(
self,
*,
namespace_path: Optional[List[str]] = None,
page_token: Optional[str] = None,
page_limit: Optional[int] = None,
) -> AsyncListing[str]:
"""Lazily list one namespace of this database's storage integrations."""
return AsyncListing(
self._inner.list_storage_integrations(
namespace_path=list(validate_namespace_path(namespace_path)),
page_token=page_token,
page_limit=page_limit,
)
)
async def drop_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> None:
"""Drop a storage integration with ownership permission."""
await self._inner.drop_storage_integration(
validate_secret_name(name), list(validate_namespace_path(namespace_path))
)
async def create_secret(
self, name: str, value: str, *, namespace_path: Optional[List[str]] = None
) -> None:
+57
View File
@@ -43,6 +43,7 @@ from ..sql import Query as SqlQuery
from ..sql import QueryDescription, QueryParameters
from ..materialized_view import MaterializedView, MaterializedViewSource, SelectArg
from ..secrets import EnvVarSecret, SecretInfo
from ..storage_integration import SecretReference, StorageIntegrationInfo
from ..view import ViewDescription
if TYPE_CHECKING:
@@ -1036,6 +1037,62 @@ class RemoteDBConnection(DBConnection):
)
return dropped, Job(job)
@override
def create_storage_integration(
self,
name: str,
storage_prefix: str,
*,
aws_role_arn: Optional[str] = None,
aws_access_key_id_secret: Optional[Union[str, SecretReference]] = None,
aws_secret_access_key_secret: Optional[Union[str, SecretReference]] = None,
aws_region: Optional[str] = None,
namespace_path: Optional[List[str]] = None,
) -> StorageIntegrationInfo:
return LOOP.run(
self._conn.create_storage_integration(
name,
storage_prefix,
aws_role_arn=aws_role_arn,
aws_access_key_id_secret=aws_access_key_id_secret,
aws_secret_access_key_secret=aws_secret_access_key_secret,
aws_region=aws_region,
namespace_path=namespace_path,
)
)
@override
def describe_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> StorageIntegrationInfo:
return LOOP.run(
self._conn.describe_storage_integration(name, namespace_path=namespace_path)
)
@override
def list_storage_integrations(
self,
*,
namespace_path: Optional[List[str]] = None,
page_token: Optional[str] = None,
page_limit: Optional[int] = None,
) -> Listing[str]:
return Listing(
self._conn.list_storage_integrations(
namespace_path=namespace_path,
page_token=page_token,
page_limit=page_limit,
)
)
@override
def drop_storage_integration(
self, name: str, *, namespace_path: Optional[List[str]] = None
) -> None:
LOOP.run(
self._conn.drop_storage_integration(name, namespace_path=namespace_path)
)
@override
def create_secret(
self, name: str, value: str, *, namespace_path: Optional[List[str]] = None
@@ -0,0 +1,52 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Database-owned storage integrations and references to credential Secrets."""
from dataclasses import dataclass
from typing import Any, Dict, Literal, Optional
@dataclass(frozen=True)
class SecretReference:
"""A credential Secret, optionally qualified by database and namespace."""
name: str
database: Optional[str] = None
namespace_path: tuple[str, ...] = ()
@dataclass(frozen=True)
class StorageIntegrationInfo:
"""Registered S3 configuration and policies to give to your AWS administrator.
``configuration_instructions`` is a single sentence of setup guidance for
display; its wording may change. Use the structured identity and policy fields
instead of parsing that text. Creating this object does not verify AWS access
or configure a storage operation to use it. Static-key authentication stores
only Secret references. No field contains AWS credentials.
"""
name: str
database: str
namespace_path: list[str]
storage_provider: Literal["S3"]
storage_prefix: str
aws_region: Optional[str]
aws_account_id: str
aws_iam_principal_arn: str
aws_external_id: str
created_at_millis: int
access_verified: bool
trust_policy: Optional[Dict[str, Any]]
permissions_policy: Dict[str, Any]
configuration_instructions: str
aws_role_arn: Optional[str] = None
aws_access_key_id_secret: Optional[SecretReference] = None
aws_secret_access_key_secret: Optional[SecretReference] = None
def __post_init__(self):
for field in ("aws_access_key_id_secret", "aws_secret_access_key_secret"):
value = getattr(self, field)
if isinstance(value, dict):
object.__setattr__(self, field, SecretReference(**value))
@@ -0,0 +1,207 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import contextlib
import json
from pathlib import Path
import pytest
import pyarrow as pa
import pyarrow.flight as flight
import lancedb
from lancedb.authz import Object, Privilege
from lancedb.db import AsyncConnection, DBConnection
from lancedb.catalog import Catalog, AsyncCatalog
from lancedb.storage_integration import SecretReference, StorageIntegrationInfo
INFO = json.loads(
(
Path(__file__).parents[3]
/ "rust/lancedb/tests/fixtures/storage_integration.json"
).read_text()
)
NAME = "raw"
ROLE = "arn:aws:iam::123456789012:role/access"
@contextlib.contextmanager
def remote_server(text_type=pa.string()):
calls = []
info = dict(INFO)
class Server(flight.FlightServerBase):
def __init__(self):
super().__init__("grpc://127.0.0.1:0")
def do_get(self, context, ticket):
command = ticket.ticket
calls.append(command)
if b"SHOW STORAGE INTEGRATIONS" in command:
result = pa.table({"name": pa.array(["zeta", NAME], type=text_type)})
elif b"DROP STORAGE INTEGRATION" in command:
result = pa.table({"dropped": [True]})
else:
if b"STORAGE_AWS_ACCESS_KEY_ID_SECRET" in command:
info["aws_role_arn"] = None
info["trust_policy"] = None
info["aws_access_key_id_secret"] = {
"database": "shared",
"namespace_path": ["credentials"],
"name": "access_key",
}
info["aws_secret_access_key_secret"] = {
"database": "shared",
"namespace_path": ["credentials"],
"name": "secret_key",
}
row = dict(info)
for field in [
"namespace_path",
"trust_policy",
"permissions_policy",
"aws_access_key_id_secret",
"aws_secret_access_key_secret",
]:
if field in row and row[field] is not None:
row[field] = json.dumps(row[field])
row.setdefault("aws_access_key_id_secret", None)
row.setdefault("aws_secret_access_key_secret", None)
result = pa.table(
{
field: pa.array(
[value],
type=(
pa.bool_()
if field == "access_verified"
else pa.int64()
if field == "created_at_millis"
else text_type
),
)
for field, value in row.items()
}
)
return flight.RecordBatchStream(result)
with Server() as server:
yield f"grpc://127.0.0.1:{server.port}", calls
def connect(host):
return lancedb.connect(
"db://default",
api_key="fake",
sql_host_override=host,
host_override="http://127.0.0.1:1",
)
def test_storage_integration_methods_belong_to_databases():
assert hasattr(DBConnection, "create_storage_integration")
assert hasattr(AsyncConnection, "create_storage_integration")
assert not hasattr(Catalog, "create_storage_integration")
assert not hasattr(AsyncCatalog, "create_storage_integration")
@pytest.mark.parametrize(
"text_type", [pa.string(), pa.large_string(), pa.string_view()]
)
def test_sync_storage_integration_lifecycle_and_aws_information(text_type):
with remote_server(text_type) as (host, calls):
db = connect(host)
info = db.create_storage_integration(
NAME, INFO["storage_prefix"], aws_role_arn=ROLE
)
assert isinstance(info, lancedb.StorageIntegrationInfo)
assert info.aws_account_id == "001122334455"
assert not info.access_verified
assert (
info.trust_policy["Statement"][0]["Condition"]["StringEquals"][
"sts:ExternalId"
]
== info.aws_external_id
)
assert db.describe_storage_integration(NAME) == info
listing = db.list_storage_integrations(page_limit=1)
assert len(calls) == 2
assert list(listing) == ["raw", "zeta"]
assert db.drop_storage_integration(NAME) is None
assert b'CREATE STORAGE INTEGRATION "raw"' in calls[0]
assert b"STORAGE_AWS_ROLE_ARN = '" + ROLE.encode() + b"'" in calls[0]
assert b'DROP STORAGE INTEGRATION "raw"' in calls[-1]
@pytest.mark.asyncio
async def test_async_storage_integration_lifecycle():
with remote_server() as (host, _calls):
db = await lancedb.connect_async(
"db://default",
api_key="fake",
sql_host_override=host,
host_override="http://127.0.0.1:1",
)
info = await db.create_storage_integration(
NAME, INFO["storage_prefix"], aws_role_arn=ROLE
)
assert await db.describe_storage_integration(NAME) == info
assert [name async for name in db.list_storage_integrations()] == [
"raw",
"zeta",
]
await db.drop_storage_integration(NAME)
with pytest.raises(ValueError):
await db.describe_storage_integration("a//b")
def test_integration_authorization_object_and_creation_privilege():
value = Object.storage_integration(database="default", name=NAME)
assert str(value) == "storage_integration:default:public$raw"
assert Privilege.CREATE_STORAGE_INTEGRATION.value == "CREATE_STORAGE_INTEGRATION"
@pytest.mark.parametrize(
"text_type", [pa.string(), pa.large_string(), pa.string_view()]
)
def test_static_credentials_use_explicit_cross_database_secret_references(text_type):
with remote_server(text_type) as (host, calls):
db = connect(host)
info = db.create_storage_integration(
"raw",
INFO["storage_prefix"],
aws_access_key_id_secret=SecretReference(
"access_key", database="shared", namespace_path=("credentials",)
),
aws_secret_access_key_secret=SecretReference(
"secret_key", database="shared", namespace_path=("credentials",)
),
)
assert info.aws_role_arn is None
assert db.describe_storage_integration("raw") == info
assert info.aws_access_key_id_secret.database == "shared"
assert b"STORAGE_AWS_ROLE_ARN" not in calls[0]
assert (
b'STORAGE_AWS_ACCESS_KEY_ID_SECRET = "shared"."credentials"."access_key"'
in calls[0]
)
with pytest.raises(ValueError):
db.create_storage_integration(
"bad",
INFO["storage_prefix"],
aws_role_arn=ROLE,
aws_access_key_id_secret="key",
)
assert len(calls) == 2
def test_metadata_defaults_an_omitted_role_to_none():
data = dict(INFO)
data.pop("aws_role_arn")
assert StorageIntegrationInfo(**data).aws_role_arn is None
def test_namespace_connections_keep_existing_apis_available(tmp_path):
db = lancedb.connect_namespace("dir", {"root": str(tmp_path)})
with pytest.raises(NotImplementedError):
db.create_storage_integration("raw", "s3://customer/raw/", aws_role_arn=ROLE)
+13
View File
@@ -262,6 +262,19 @@ impl AuthzObject {
}))
}
#[staticmethod]
fn storage_integration(
database: &str,
namespace: &Bound<'_, PyAny>,
name: &str,
) -> PyResult<Self> {
Self::validated(Object::StorageIntegration(StorageIntegrationObject {
database: database.into(),
namespace: namespace_path(namespace)?,
storage_integration: name.into(),
}))
}
#[staticmethod]
fn function(database: &str, namespace: &Bound<'_, PyAny>, name: &str) -> PyResult<Self> {
Self::validated(Object::Function(FunctionObject {
+66
View File
@@ -888,6 +888,72 @@ impl Connection {
})
}
#[pyo3(signature = (name, request, namespace_path=None))]
pub fn create_storage_integration<'py>(
&self,
py: Python<'py>,
name: String,
request: String,
namespace_path: Option<Vec<String>>,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self.get_inner()?.clone();
let request: lancedb::storage_integration::CreateStorageIntegrationRequest =
serde_json::from_str(&request)
.map_err(|error| PyValueError::new_err(error.to_string()))?;
future_into_py(py, async move {
let info = inner
.create_storage_integration(name, request, &namespace_path.unwrap_or_default())
.await
.infer_error()?;
serde_json::to_string(&info).map_err(|error| PyValueError::new_err(error.to_string()))
})
}
#[pyo3(signature = (name, namespace_path=None))]
pub fn describe_storage_integration<'py>(
&self,
py: Python<'py>,
name: String,
namespace_path: Option<Vec<String>>,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self.get_inner()?.clone();
future_into_py(py, async move {
let info = inner
.describe_storage_integration(name, &namespace_path.unwrap_or_default())
.await
.infer_error()?;
serde_json::to_string(&info).map_err(|error| PyValueError::new_err(error.to_string()))
})
}
#[pyo3(signature = (*, namespace_path=None, page_token=None, page_limit=None))]
pub fn list_storage_integrations(
&self,
namespace_path: Option<Vec<String>>,
page_token: Option<String>,
page_limit: Option<u32>,
) -> PyResult<crate::listing::NameListing> {
Ok(crate::listing::NameListing::new(
self.get_inner()?.list_storage_integrations(
&namespace_path.unwrap_or_default(),
crate::listing::options(page_token, page_limit),
),
))
}
#[pyo3(signature = (name, namespace_path=None))]
pub fn drop_storage_integration<'py>(
&self,
py: Python<'py>,
name: String,
namespace_path: Option<Vec<String>>,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self.get_inner()?.clone();
future_into_py(py, async move {
inner
.drop_storage_integration(name, &namespace_path.unwrap_or_default())
.await
.infer_error()
})
}
#[pyo3(signature = (name, value, namespace_path=None))]
pub fn create_secret(
self_: PyRef<'_, Self>,