mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-07-20 11:47:00 +00:00
feat: enable auth for LocalFS Files Provider (#2773)
Some checks failed
Integration Tests / discover-tests (push) Successful in 4s
Integration Auth Tests / test-matrix (oauth2_token) (push) Failing after 7s
Test Llama Stack Build / generate-matrix (push) Successful in 7s
Coverage Badge / unit-tests (push) Failing after 16s
Test Llama Stack Build / build-single-provider (push) Failing after 11s
Vector IO Integration Tests / test-matrix (3.13, inline::milvus) (push) Failing after 16s
Unit Tests / unit-tests (3.12) (push) Failing after 13s
Test External Providers / test-external-providers (venv) (push) Failing after 12s
Vector IO Integration Tests / test-matrix (3.12, inline::faiss) (push) Failing after 17s
Vector IO Integration Tests / test-matrix (3.13, inline::faiss) (push) Failing after 16s
Python Package Build Test / build (3.12) (push) Failing after 13s
Test Llama Stack Build / build-custom-container-distribution (push) Failing after 17s
SqlStore Integration Tests / test-postgres (3.12) (push) Failing after 23s
Vector IO Integration Tests / test-matrix (3.12, inline::milvus) (push) Failing after 23s
Vector IO Integration Tests / test-matrix (3.13, remote::pgvector) (push) Failing after 17s
Update ReadTheDocs / update-readthedocs (push) Failing after 19s
Vector IO Integration Tests / test-matrix (3.13, inline::sqlite-vec) (push) Failing after 23s
Test Llama Stack Build / build-ubi9-container-distribution (push) Failing after 21s
Vector IO Integration Tests / test-matrix (3.12, remote::chromadb) (push) Failing after 18s
Unit Tests / unit-tests (3.13) (push) Failing after 20s
Vector IO Integration Tests / test-matrix (3.13, remote::chromadb) (push) Failing after 23s
Test Llama Stack Build / build (push) Failing after 16s
Vector IO Integration Tests / test-matrix (3.12, inline::sqlite-vec) (push) Failing after 25s
Python Package Build Test / build (3.13) (push) Failing after 2m19s
Vector IO Integration Tests / test-matrix (3.12, remote::pgvector) (push) Failing after 2m25s
SqlStore Integration Tests / test-postgres (3.13) (push) Failing after 2m32s
Integration Tests / test-matrix (push) Failing after 2m24s
Pre-commit / pre-commit (push) Successful in 3m57s
Some checks failed
Integration Tests / discover-tests (push) Successful in 4s
Integration Auth Tests / test-matrix (oauth2_token) (push) Failing after 7s
Test Llama Stack Build / generate-matrix (push) Successful in 7s
Coverage Badge / unit-tests (push) Failing after 16s
Test Llama Stack Build / build-single-provider (push) Failing after 11s
Vector IO Integration Tests / test-matrix (3.13, inline::milvus) (push) Failing after 16s
Unit Tests / unit-tests (3.12) (push) Failing after 13s
Test External Providers / test-external-providers (venv) (push) Failing after 12s
Vector IO Integration Tests / test-matrix (3.12, inline::faiss) (push) Failing after 17s
Vector IO Integration Tests / test-matrix (3.13, inline::faiss) (push) Failing after 16s
Python Package Build Test / build (3.12) (push) Failing after 13s
Test Llama Stack Build / build-custom-container-distribution (push) Failing after 17s
SqlStore Integration Tests / test-postgres (3.12) (push) Failing after 23s
Vector IO Integration Tests / test-matrix (3.12, inline::milvus) (push) Failing after 23s
Vector IO Integration Tests / test-matrix (3.13, remote::pgvector) (push) Failing after 17s
Update ReadTheDocs / update-readthedocs (push) Failing after 19s
Vector IO Integration Tests / test-matrix (3.13, inline::sqlite-vec) (push) Failing after 23s
Test Llama Stack Build / build-ubi9-container-distribution (push) Failing after 21s
Vector IO Integration Tests / test-matrix (3.12, remote::chromadb) (push) Failing after 18s
Unit Tests / unit-tests (3.13) (push) Failing after 20s
Vector IO Integration Tests / test-matrix (3.13, remote::chromadb) (push) Failing after 23s
Test Llama Stack Build / build (push) Failing after 16s
Vector IO Integration Tests / test-matrix (3.12, inline::sqlite-vec) (push) Failing after 25s
Python Package Build Test / build (3.13) (push) Failing after 2m19s
Vector IO Integration Tests / test-matrix (3.12, remote::pgvector) (push) Failing after 2m25s
SqlStore Integration Tests / test-postgres (3.13) (push) Failing after 2m32s
Integration Tests / test-matrix (push) Failing after 2m24s
Pre-commit / pre-commit (push) Successful in 3m57s
# What does this PR do? Supports authentication for LocalFS Files provider. closes https://github.com/meta-llama/llama-stack/issues/2760 ## Test Plan CI. added tests.
This commit is contained in:
parent
dd303327f3
commit
0a6e588f68
4 changed files with 233 additions and 11 deletions
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from llama_stack.distribution.datatypes import Api
|
from llama_stack.distribution.datatypes import AccessRule, Api
|
||||||
|
|
||||||
from .config import LocalfsFilesImplConfig
|
from .config import LocalfsFilesImplConfig
|
||||||
from .files import LocalfsFilesImpl
|
from .files import LocalfsFilesImpl
|
||||||
|
@ -14,7 +14,7 @@ from .files import LocalfsFilesImpl
|
||||||
__all__ = ["LocalfsFilesImpl", "LocalfsFilesImplConfig"]
|
__all__ = ["LocalfsFilesImpl", "LocalfsFilesImplConfig"]
|
||||||
|
|
||||||
|
|
||||||
async def get_provider_impl(config: LocalfsFilesImplConfig, deps: dict[Api, Any]):
|
async def get_provider_impl(config: LocalfsFilesImplConfig, deps: dict[Api, Any], policy: list[AccessRule]):
|
||||||
impl = LocalfsFilesImpl(config)
|
impl = LocalfsFilesImpl(config, policy)
|
||||||
await impl.initialize()
|
await impl.initialize()
|
||||||
return impl
|
return impl
|
||||||
|
|
|
@ -19,16 +19,19 @@ from llama_stack.apis.files import (
|
||||||
OpenAIFileObject,
|
OpenAIFileObject,
|
||||||
OpenAIFilePurpose,
|
OpenAIFilePurpose,
|
||||||
)
|
)
|
||||||
|
from llama_stack.distribution.datatypes import AccessRule
|
||||||
from llama_stack.providers.utils.sqlstore.api import ColumnDefinition, ColumnType
|
from llama_stack.providers.utils.sqlstore.api import ColumnDefinition, ColumnType
|
||||||
from llama_stack.providers.utils.sqlstore.sqlstore import SqlStore, sqlstore_impl
|
from llama_stack.providers.utils.sqlstore.authorized_sqlstore import AuthorizedSqlStore
|
||||||
|
from llama_stack.providers.utils.sqlstore.sqlstore import sqlstore_impl
|
||||||
|
|
||||||
from .config import LocalfsFilesImplConfig
|
from .config import LocalfsFilesImplConfig
|
||||||
|
|
||||||
|
|
||||||
class LocalfsFilesImpl(Files):
|
class LocalfsFilesImpl(Files):
|
||||||
def __init__(self, config: LocalfsFilesImplConfig) -> None:
|
def __init__(self, config: LocalfsFilesImplConfig, policy: list[AccessRule]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.sql_store: SqlStore | None = None
|
self.policy = policy
|
||||||
|
self.sql_store: AuthorizedSqlStore | None = None
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
"""Initialize the files provider by setting up storage directory and metadata database."""
|
"""Initialize the files provider by setting up storage directory and metadata database."""
|
||||||
|
@ -37,7 +40,7 @@ class LocalfsFilesImpl(Files):
|
||||||
storage_path.mkdir(parents=True, exist_ok=True)
|
storage_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Initialize SQL store for metadata
|
# Initialize SQL store for metadata
|
||||||
self.sql_store = sqlstore_impl(self.config.metadata_store)
|
self.sql_store = AuthorizedSqlStore(sqlstore_impl(self.config.metadata_store))
|
||||||
await self.sql_store.create_table(
|
await self.sql_store.create_table(
|
||||||
"openai_files",
|
"openai_files",
|
||||||
{
|
{
|
||||||
|
@ -126,6 +129,7 @@ class LocalfsFilesImpl(Files):
|
||||||
|
|
||||||
paginated_result = await self.sql_store.fetch_all(
|
paginated_result = await self.sql_store.fetch_all(
|
||||||
table="openai_files",
|
table="openai_files",
|
||||||
|
policy=self.policy,
|
||||||
where=where_conditions if where_conditions else None,
|
where=where_conditions if where_conditions else None,
|
||||||
order_by=[("created_at", order.value)],
|
order_by=[("created_at", order.value)],
|
||||||
cursor=("id", after) if after else None,
|
cursor=("id", after) if after else None,
|
||||||
|
@ -156,7 +160,7 @@ class LocalfsFilesImpl(Files):
|
||||||
if not self.sql_store:
|
if not self.sql_store:
|
||||||
raise RuntimeError("Files provider not initialized")
|
raise RuntimeError("Files provider not initialized")
|
||||||
|
|
||||||
row = await self.sql_store.fetch_one("openai_files", where={"id": file_id})
|
row = await self.sql_store.fetch_one("openai_files", policy=self.policy, where={"id": file_id})
|
||||||
if not row:
|
if not row:
|
||||||
raise ValueError(f"File with id {file_id} not found")
|
raise ValueError(f"File with id {file_id} not found")
|
||||||
|
|
||||||
|
@ -174,7 +178,7 @@ class LocalfsFilesImpl(Files):
|
||||||
if not self.sql_store:
|
if not self.sql_store:
|
||||||
raise RuntimeError("Files provider not initialized")
|
raise RuntimeError("Files provider not initialized")
|
||||||
|
|
||||||
row = await self.sql_store.fetch_one("openai_files", where={"id": file_id})
|
row = await self.sql_store.fetch_one("openai_files", policy=self.policy, where={"id": file_id})
|
||||||
if not row:
|
if not row:
|
||||||
raise ValueError(f"File with id {file_id} not found")
|
raise ValueError(f"File with id {file_id} not found")
|
||||||
|
|
||||||
|
@ -197,7 +201,7 @@ class LocalfsFilesImpl(Files):
|
||||||
raise RuntimeError("Files provider not initialized")
|
raise RuntimeError("Files provider not initialized")
|
||||||
|
|
||||||
# Get file metadata
|
# Get file metadata
|
||||||
row = await self.sql_store.fetch_one("openai_files", where={"id": file_id})
|
row = await self.sql_store.fetch_one("openai_files", policy=self.policy, where={"id": file_id})
|
||||||
if not row:
|
if not row:
|
||||||
raise ValueError(f"File with id {file_id} not found")
|
raise ValueError(f"File with id {file_id} not found")
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
# the root directory of this source tree.
|
# the root directory of this source tree.
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
|
||||||
|
from llama_stack.distribution.datatypes import User
|
||||||
from llama_stack.distribution.library_client import LlamaStackAsLibraryClient
|
from llama_stack.distribution.library_client import LlamaStackAsLibraryClient
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,3 +63,218 @@ def test_openai_client_basic_operations(compat_client, client_with_models):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
@patch("llama_stack.providers.utils.sqlstore.authorized_sqlstore.get_authenticated_user")
|
||||||
|
def test_files_authentication_isolation(mock_get_authenticated_user, compat_client, client_with_models):
|
||||||
|
"""Test that users can only access their own files."""
|
||||||
|
if isinstance(client_with_models, LlamaStackAsLibraryClient) and isinstance(compat_client, OpenAI):
|
||||||
|
pytest.skip("OpenAI files are not supported when testing with LlamaStackAsLibraryClient")
|
||||||
|
if not isinstance(client_with_models, LlamaStackAsLibraryClient):
|
||||||
|
pytest.skip("Authentication tests require LlamaStackAsLibraryClient (library mode)")
|
||||||
|
|
||||||
|
client = compat_client
|
||||||
|
|
||||||
|
# Create two test users
|
||||||
|
user1 = User("user1", {"roles": ["user"], "teams": ["team-a"]})
|
||||||
|
user2 = User("user2", {"roles": ["user"], "teams": ["team-b"]})
|
||||||
|
|
||||||
|
# User 1 uploads a file
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
test_content_1 = b"User 1's private file content"
|
||||||
|
|
||||||
|
with BytesIO(test_content_1) as file_buffer:
|
||||||
|
file_buffer.name = "user1_file.txt"
|
||||||
|
user1_file = client.files.create(file=file_buffer, purpose="assistants")
|
||||||
|
|
||||||
|
# User 2 uploads a file
|
||||||
|
mock_get_authenticated_user.return_value = user2
|
||||||
|
test_content_2 = b"User 2's private file content"
|
||||||
|
|
||||||
|
with BytesIO(test_content_2) as file_buffer:
|
||||||
|
file_buffer.name = "user2_file.txt"
|
||||||
|
user2_file = client.files.create(file=file_buffer, purpose="assistants")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# User 1 can see their own file
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
user1_files = client.files.list()
|
||||||
|
user1_file_ids = [f.id for f in user1_files.data]
|
||||||
|
assert user1_file.id in user1_file_ids
|
||||||
|
assert user2_file.id not in user1_file_ids # Cannot see user2's file
|
||||||
|
|
||||||
|
# User 2 can see their own file
|
||||||
|
mock_get_authenticated_user.return_value = user2
|
||||||
|
user2_files = client.files.list()
|
||||||
|
user2_file_ids = [f.id for f in user2_files.data]
|
||||||
|
assert user2_file.id in user2_file_ids
|
||||||
|
assert user1_file.id not in user2_file_ids # Cannot see user1's file
|
||||||
|
|
||||||
|
# User 1 can retrieve their own file
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
retrieved_file = client.files.retrieve(user1_file.id)
|
||||||
|
assert retrieved_file.id == user1_file.id
|
||||||
|
|
||||||
|
# User 1 cannot retrieve user2's file
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
client.files.retrieve(user2_file.id)
|
||||||
|
|
||||||
|
# User 1 can access their file content
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
content_response = client.files.content(user1_file.id)
|
||||||
|
if isinstance(content_response, str):
|
||||||
|
content = bytes(content_response, "utf-8")
|
||||||
|
else:
|
||||||
|
content = content_response.content
|
||||||
|
assert content == test_content_1
|
||||||
|
|
||||||
|
# User 1 cannot access user2's file content
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
client.files.content(user2_file.id)
|
||||||
|
|
||||||
|
# User 1 can delete their own file
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
delete_response = client.files.delete(user1_file.id)
|
||||||
|
assert delete_response.deleted is True
|
||||||
|
|
||||||
|
# User 1 cannot delete user2's file
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
client.files.delete(user2_file.id)
|
||||||
|
|
||||||
|
# User 2 can still access their file after user1's file is deleted
|
||||||
|
mock_get_authenticated_user.return_value = user2
|
||||||
|
retrieved_file = client.files.retrieve(user2_file.id)
|
||||||
|
assert retrieved_file.id == user2_file.id
|
||||||
|
|
||||||
|
# Cleanup user2's file
|
||||||
|
mock_get_authenticated_user.return_value = user2
|
||||||
|
client.files.delete(user2_file.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Cleanup in case of failure
|
||||||
|
try:
|
||||||
|
mock_get_authenticated_user.return_value = user1
|
||||||
|
client.files.delete(user1_file.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
mock_get_authenticated_user.return_value = user2
|
||||||
|
client.files.delete(user2_file.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
@patch("llama_stack.providers.utils.sqlstore.authorized_sqlstore.get_authenticated_user")
|
||||||
|
def test_files_authentication_shared_attributes(mock_get_authenticated_user, compat_client, client_with_models):
|
||||||
|
"""Test access control with users having identical attributes."""
|
||||||
|
if isinstance(client_with_models, LlamaStackAsLibraryClient) and isinstance(compat_client, OpenAI):
|
||||||
|
pytest.skip("OpenAI files are not supported when testing with LlamaStackAsLibraryClient")
|
||||||
|
if not isinstance(client_with_models, LlamaStackAsLibraryClient):
|
||||||
|
pytest.skip("Authentication tests require LlamaStackAsLibraryClient (library mode)")
|
||||||
|
|
||||||
|
client = compat_client
|
||||||
|
|
||||||
|
# Create users with identical attributes (required for default policy)
|
||||||
|
user_a = User("user-a", {"roles": ["user"], "teams": ["shared-team"]})
|
||||||
|
user_b = User("user-b", {"roles": ["user"], "teams": ["shared-team"]})
|
||||||
|
|
||||||
|
# User A uploads a file
|
||||||
|
mock_get_authenticated_user.return_value = user_a
|
||||||
|
test_content = b"Shared attributes file content"
|
||||||
|
|
||||||
|
with BytesIO(test_content) as file_buffer:
|
||||||
|
file_buffer.name = "shared_attributes_file.txt"
|
||||||
|
shared_file = client.files.create(file=file_buffer, purpose="assistants")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# User B with identical attributes can access the file
|
||||||
|
mock_get_authenticated_user.return_value = user_b
|
||||||
|
files_list = client.files.list()
|
||||||
|
file_ids = [f.id for f in files_list.data]
|
||||||
|
|
||||||
|
# User B should be able to see the file due to identical attributes
|
||||||
|
assert shared_file.id in file_ids
|
||||||
|
|
||||||
|
# User B can retrieve file info
|
||||||
|
retrieved_file = client.files.retrieve(shared_file.id)
|
||||||
|
assert retrieved_file.id == shared_file.id
|
||||||
|
|
||||||
|
# User B can access file content
|
||||||
|
content_response = client.files.content(shared_file.id)
|
||||||
|
if isinstance(content_response, str):
|
||||||
|
content = bytes(content_response, "utf-8")
|
||||||
|
else:
|
||||||
|
content = content_response.content
|
||||||
|
assert content == test_content
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
mock_get_authenticated_user.return_value = user_a
|
||||||
|
client.files.delete(shared_file.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Cleanup in case of failure
|
||||||
|
try:
|
||||||
|
mock_get_authenticated_user.return_value = user_a
|
||||||
|
client.files.delete(shared_file.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
mock_get_authenticated_user.return_value = user_b
|
||||||
|
client.files.delete(shared_file.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
@patch("llama_stack.providers.utils.sqlstore.authorized_sqlstore.get_authenticated_user")
|
||||||
|
def test_files_authentication_anonymous_access(mock_get_authenticated_user, compat_client, client_with_models):
|
||||||
|
"""Test anonymous user behavior when no authentication is present."""
|
||||||
|
if isinstance(client_with_models, LlamaStackAsLibraryClient) and isinstance(compat_client, OpenAI):
|
||||||
|
pytest.skip("OpenAI files are not supported when testing with LlamaStackAsLibraryClient")
|
||||||
|
if not isinstance(client_with_models, LlamaStackAsLibraryClient):
|
||||||
|
pytest.skip("Authentication tests require LlamaStackAsLibraryClient (library mode)")
|
||||||
|
|
||||||
|
client = compat_client
|
||||||
|
|
||||||
|
# Simulate anonymous user (no authentication)
|
||||||
|
mock_get_authenticated_user.return_value = None
|
||||||
|
|
||||||
|
test_content = b"Anonymous file content"
|
||||||
|
|
||||||
|
with BytesIO(test_content) as file_buffer:
|
||||||
|
file_buffer.name = "anonymous_file.txt"
|
||||||
|
anonymous_file = client.files.create(file=file_buffer, purpose="assistants")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Anonymous user should be able to access their own uploaded file
|
||||||
|
files_list = client.files.list()
|
||||||
|
file_ids = [f.id for f in files_list.data]
|
||||||
|
assert anonymous_file.id in file_ids
|
||||||
|
|
||||||
|
# Can retrieve file info
|
||||||
|
retrieved_file = client.files.retrieve(anonymous_file.id)
|
||||||
|
assert retrieved_file.id == anonymous_file.id
|
||||||
|
|
||||||
|
# Can access file content
|
||||||
|
content_response = client.files.content(anonymous_file.id)
|
||||||
|
if isinstance(content_response, str):
|
||||||
|
content = bytes(content_response, "utf-8")
|
||||||
|
else:
|
||||||
|
content = content_response.content
|
||||||
|
assert content == test_content
|
||||||
|
|
||||||
|
# Can delete the file
|
||||||
|
delete_response = client.files.delete(anonymous_file.id)
|
||||||
|
assert delete_response.deleted is True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Cleanup in case of failure
|
||||||
|
try:
|
||||||
|
client.files.delete(anonymous_file.id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise e
|
||||||
|
|
|
@ -9,6 +9,7 @@ import pytest
|
||||||
|
|
||||||
from llama_stack.apis.common.responses import Order
|
from llama_stack.apis.common.responses import Order
|
||||||
from llama_stack.apis.files import OpenAIFilePurpose
|
from llama_stack.apis.files import OpenAIFilePurpose
|
||||||
|
from llama_stack.distribution.access_control.access_control import default_policy
|
||||||
from llama_stack.providers.inline.files.localfs import (
|
from llama_stack.providers.inline.files.localfs import (
|
||||||
LocalfsFilesImpl,
|
LocalfsFilesImpl,
|
||||||
LocalfsFilesImplConfig,
|
LocalfsFilesImplConfig,
|
||||||
|
@ -38,7 +39,7 @@ async def files_provider(tmp_path):
|
||||||
storage_dir=storage_dir.as_posix(), metadata_store=SqliteSqlStoreConfig(db_path=db_path.as_posix())
|
storage_dir=storage_dir.as_posix(), metadata_store=SqliteSqlStoreConfig(db_path=db_path.as_posix())
|
||||||
)
|
)
|
||||||
|
|
||||||
provider = LocalfsFilesImpl(config)
|
provider = LocalfsFilesImpl(config, default_policy())
|
||||||
await provider.initialize()
|
await provider.initialize()
|
||||||
yield provider
|
yield provider
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue