diff --git a/docs/supported-databases/azure_blob_storage.rst b/docs/supported-databases/azure_blob_storage.rst index 2082231..1599a3c 100644 --- a/docs/supported-databases/azure_blob_storage.rst +++ b/docs/supported-databases/azure_blob_storage.rst @@ -1,24 +1,14 @@ Azure Blob Storage ================== -Integration with `Azure Blob Storage `_, a cloud-based object storage service. - -This integration uses the official `Azure Storage Blobs Python Client `_ to interact with Azure Blob Storage, which provides scalable object storage for testing and development. +Integration with `Azure Blob Storage `_. Installation ------------ .. code-block:: bash - pip install pytest-databases[azure] - -Configuration -------------- - -* ``AZURE_STORAGE_CONNECTION_STRING``: Connection string for Azure Blob Storage -* ``AZURE_STORAGE_ACCOUNT_NAME``: Account name for Azure Blob Storage -* ``AZURE_STORAGE_ACCOUNT_KEY``: Account key for Azure Blob Storage -* ``AZURE_STORAGE_CONTAINER_NAME``: Container name for Azure Blob Storage (default: "pytest-databases") + pip install pytest-databases[azure-storage] Usage Example ------------- @@ -26,31 +16,28 @@ Usage Example .. code-block:: python import pytest - from azure.storage.blob import BlobServiceClient - from pytest_databases.docker.azure_blob import AzureBlobStorageService - pytest_plugins = ["pytest_databases.docker.azure_blob"] + from azure.storage.blob import ContainerClient + from pytest_databases.docker.azure_blob import AzureBlobService - def test(azure_blob_storage_service: AzureBlobStorageService) -> None: - client = BlobServiceClient.from_connection_string( - azure_blob_storage_service.connection_string - ) - container = client.get_container_client(azure_blob_storage_service.container_name) - container.create_container() - assert container.exists() + pytest_plugins = ["pytest_databases.docker.azure_blob"] - def test(azure_blob_storage_client: BlobServiceClient) -> None: - container = azure_blob_storage_client.get_container_client("test-container") - container.create_container() - assert container.exists() + def test( + azure_blob_service: AzureBlobService, + azure_blob_default_container_name: str, + ) -> None: + with ContainerClient.from_connection_string( + azure_blob_service.connection_string, + container_name=azure_blob_default_container_name, + ) as container: + assert container.exists() Available Fixtures ------------------ -* ``azurite_in_memory``: Whether to use in-memory storage for Azurite (default: ``True``) -* ``azure_blob_service``: A fixture that provides an Azure Blob Storage service. -* ``azure_blob_default_container_name``: The default container name for Azure Blob Storage (default: ``pytest-databases``) -* ``azure_blob_container_client``: A fixture that provides an Azure Blob Storage container client. -* ``azure_blob_async_container_client``: A fixture that provides an Azure Blob Storage container client for async operations. +* ``azurite_in_memory``: Whether to use in-memory storage for Azurite (default: ``True``). +* ``azure_blob_service``: A fixture that provides an Azure Blob Storage service with the default container pre-created. +* ``azure_blob_default_container_name``: The default container name for Azure Blob Storage (default: ``pytest-databases``). +* ``azure_blob_xdist_isolation_level``: Xdist isolation level for the service (default: ``database``). Service API ----------- diff --git a/pyproject.toml b/pyproject.toml index af70e1f..3256ea9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ Issues = "https://github.com/litestar-org/pytest-databases/issues" Source = "https://github.com/litestar-org/pytest-databases" [project.optional-dependencies] -azure-storage = ["azure-storage-blob"] +azure-storage = [] bigquery = ["google-cloud-bigquery"] cockroachdb = ["psycopg"] dragonfly = ["redis"] diff --git a/src/pytest_databases/docker/azure_blob.py b/src/pytest_databases/docker/azure_blob.py index 8779159..8221cb4 100644 --- a/src/pytest_databases/docker/azure_blob.py +++ b/src/pytest_databases/docker/azure_blob.py @@ -4,20 +4,42 @@ from typing import TYPE_CHECKING import pytest -from azure.storage.blob import ContainerClient -from azure.storage.blob.aio import ContainerClient as AsyncContainerClient from pytest_databases.helpers import get_xdist_worker_count, get_xdist_worker_num from pytest_databases.types import ServiceContainer, XdistIsolationLevel if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Generator + from collections.abc import Generator from pytest_databases._service import DockerService DEFAULT_ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" DEFAULT_ACCOUNT_NAME = "devstoreaccount1" +AZURE_CLI_IMAGE = "mcr.microsoft.com/azure-cli:latest" + + +def _bootstrap_azure_blob_container( + docker_service: DockerService, + *, + connection_string: str, + container_name: str, +) -> None: + docker_service._client.containers.run( + AZURE_CLI_IMAGE, + [ + "az", + "storage", + "container", + "create", + "--name", + container_name, + "--connection-string", + connection_string, + ], + network_mode="host", + remove=True, + ) @dataclass @@ -47,6 +69,7 @@ def azure_blob_service( docker_service: DockerService, azurite_in_memory: bool, azure_blob_xdist_isolation_level: XdistIsolationLevel, + azure_blob_default_container_name: str, ) -> Generator[ServiceContainer, None, None]: command = "azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck" if azurite_in_memory: @@ -83,6 +106,12 @@ def azure_blob_service( f"BlobEndpoint={account_url};" ) + _bootstrap_azure_blob_container( + docker_service, + connection_string=connection_string, + container_name=azure_blob_default_container_name, + ) + yield AzureBlobService( host=service.host, port=service.port, @@ -97,27 +126,3 @@ def azure_blob_service( @pytest.fixture(scope="session") def azure_blob_default_container_name() -> str: return "pytest-databases" - - -@pytest.fixture(scope="session") -def azure_blob_container_client( - azure_blob_service: AzureBlobService, - azure_blob_default_container_name: str, -) -> Generator[ContainerClient, None, None]: - with ContainerClient.from_connection_string( - azure_blob_service.connection_string, - container_name=azure_blob_default_container_name, - ) as container_client: - yield container_client - - -@pytest.fixture(scope="session") -async def azure_blob_async_container_client( - azure_blob_service: AzureBlobService, - azure_blob_default_container_name: str, -) -> AsyncGenerator[AsyncContainerClient, None]: - async with AsyncContainerClient.from_connection_string( - azure_blob_service.connection_string, - container_name=azure_blob_default_container_name, - ) as container_client: - yield container_client diff --git a/tests/test_azure_blob.py b/tests/test_azure_blob.py index cdfead1..19b90d0 100644 --- a/tests/test_azure_blob.py +++ b/tests/test_azure_blob.py @@ -1,31 +1,91 @@ -import pytest +from __future__ import annotations +from typing import TYPE_CHECKING -def test_default_no_xdist(pytester: pytest.Pytester) -> None: +if TYPE_CHECKING: + import pytest + + +AZURE_BLOB_TEST_HELPERS = """ +import json + + +def run_az(docker_service, service, *args): + return docker_service._client.containers.run( + "mcr.microsoft.com/azure-cli:latest", + ["az", "storage", *args, "--connection-string", service.connection_string], + network_mode="host", + remove=True, + ) + + +def az_container_exists(docker_service, service, container_name): + raw = run_az(docker_service, service, "container", "exists", "--name", container_name) + if isinstance(raw, bytes): + raw = raw.decode() + return json.loads(raw)["exists"] +""" + + +def test_plugin_imports_without_azure_storage_blob(pytester: pytest.Pytester) -> None: pytester.makepyfile(""" -import pytest -from azure.storage.blob import ContainerClient + import builtins + + def test_import() -> None: + original_import = builtins.__import__ + + def blocked_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "azure.storage.blob" or name.startswith("azure.storage.blob."): + raise ModuleNotFoundError(name) + return original_import(name, globals, locals, fromlist, level) + + builtins.__import__ = blocked_import + try: + import pytest_databases.docker.azure_blob # noqa: F401 + finally: + builtins.__import__ = original_import + """) + + result = pytester.runpytest_subprocess("-p", "pytest_databases", "-vv") + result.assert_outcomes(passed=1) + + +def test_default_no_xdist(pytester: pytest.Pytester) -> None: + pytester.makepyfile(f""" +from pytest_databases._service import DockerService +from pytest_databases.docker.azure_blob import AzureBlobService pytest_plugins = [ "pytest_databases.docker.azure_blob", ] +{AZURE_BLOB_TEST_HELPERS} -def test_one(azure_blob_container_client: ContainerClient) -> None: - azure_blob_container_client.create_container() +def test_one( + docker_service: DockerService, + azure_blob_service: AzureBlobService, + azure_blob_default_container_name: str, +) -> None: + assert az_container_exists(docker_service, azure_blob_service, azure_blob_default_container_name) -def test_two(azure_blob_container_client: ContainerClient) -> None: - assert azure_blob_container_client.exists() + +def test_two( + docker_service: DockerService, + azure_blob_service: AzureBlobService, + azure_blob_default_container_name: str, +) -> None: + assert az_container_exists(docker_service, azure_blob_service, azure_blob_default_container_name) """) result = pytester.runpytest_subprocess("-p", "pytest_databases") result.assert_outcomes(passed=2) def test_xdist_isolate_server(pytester: pytest.Pytester) -> None: - pytester.makepyfile(""" + pytester.makepyfile(f""" import pytest -from azure.storage.blob import ContainerClient +from pytest_databases._service import DockerService +from pytest_databases.docker.azure_blob import AzureBlobService pytest_plugins = [ "pytest_databases.docker.azure_blob", @@ -37,22 +97,32 @@ def azure_blob_xdist_isolation_level(): return "server" -def test_one(azure_blob_container_client: ContainerClient) -> None: - assert not azure_blob_container_client.exists() - azure_blob_container_client.create_container() +{AZURE_BLOB_TEST_HELPERS} + +def test_one( + docker_service: DockerService, + azure_blob_service: AzureBlobService, + azure_blob_default_container_name: str, +) -> None: + assert az_container_exists(docker_service, azure_blob_service, azure_blob_default_container_name) -def test_two(azure_blob_container_client: ContainerClient) -> None: - assert not azure_blob_container_client.exists() - azure_blob_container_client.create_container() + +def test_two( + docker_service: DockerService, + azure_blob_service: AzureBlobService, + azure_blob_default_container_name: str, +) -> None: + assert az_container_exists(docker_service, azure_blob_service, azure_blob_default_container_name) """) result = pytester.runpytest_subprocess("-p", "pytest_databases", "-n", "2") result.assert_outcomes(passed=2) def test_xdist_isolate_database(pytester: pytest.Pytester) -> None: - pytester.makepyfile(""" -from azure.storage.blob import ContainerClient + pytester.makepyfile(f""" +from pytest_databases._service import DockerService +from pytest_databases.docker.azure_blob import AzureBlobService from pytest_databases.helpers import get_xdist_worker_num pytest_plugins = [ @@ -60,19 +130,25 @@ def test_xdist_isolate_database(pytester: pytest.Pytester) -> None: ] -def test_one(azure_blob_container_client: ContainerClient, azure_blob_default_container_name: str) -> None: - assert not azure_blob_container_client.exists() - azure_blob_container_client.create_container() - assert azure_blob_container_client.container_name == azure_blob_default_container_name - assert azure_blob_container_client.account_name == f"test_account_{get_xdist_worker_num()}" +{AZURE_BLOB_TEST_HELPERS} + +def test_one( + docker_service: DockerService, + azure_blob_service: AzureBlobService, + azure_blob_default_container_name: str, +) -> None: + assert az_container_exists(docker_service, azure_blob_service, azure_blob_default_container_name) + assert azure_blob_service.account_name == f"test_account_{{get_xdist_worker_num()}}" -def test_two(azure_blob_container_client: ContainerClient, azure_blob_default_container_name: str) -> None: - assert not azure_blob_container_client.exists() - azure_blob_container_client.create_container() - assert azure_blob_container_client.container_name == azure_blob_default_container_name - assert azure_blob_container_client.account_name == f"test_account_{get_xdist_worker_num()}" +def test_two( + docker_service: DockerService, + azure_blob_service: AzureBlobService, + azure_blob_default_container_name: str, +) -> None: + assert az_container_exists(docker_service, azure_blob_service, azure_blob_default_container_name) + assert azure_blob_service.account_name == f"test_account_{{get_xdist_worker_num()}}" """) result = pytester.runpytest_subprocess("-p", "pytest_databases", "-n", "2") result.assert_outcomes(passed=2) diff --git a/uv.lock b/uv.lock index 7c7eeef..405173c 100644 --- a/uv.lock +++ b/uv.lock @@ -352,61 +352,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/3d/4357a0f685c0a2ae7132ac91905bec565e64f9ba63b079f7ec5da46e3597/autodocsumm-0.2.15-py3-none-any.whl", hash = "sha256:dbe6fabcaeae4540748ea9b3443eb76c2692e063d44f004f67c424610a5aca9a", size = 14852, upload-time = "2026-03-26T20:44:05.273Z" }, ] -[[package]] -name = "azure-core" -version = "1.39.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version > '3.9' and python_full_version < '3.10'", - "python_full_version <= '3.9'", -] -dependencies = [ - { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, -] - -[[package]] -name = "azure-core" -version = "1.41.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", - "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, -] - -[[package]] -name = "azure-storage-blob" -version = "12.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core", version = "1.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "azure-core", version = "1.41.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "cryptography", version = "47.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.9'" }, - { name = "cryptography", version = "48.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version > '3.9'" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/25/fdcf1e381922dbab8ba23d6fd78d397fe6cbac6b480310218834b7bc91fe/azure_storage_blob-12.29.0.tar.gz", hash = "sha256:2824ddd7ebc9056034ebc76b17971a38e9aa5835abb0d565b9700493f2a6c657", size = 611359, upload-time = "2026-05-15T03:34:59.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2c/6ddee6a3e42d0236ba9259e4df7fa97fdc415ff0802b736c634baaf4b285/azure_storage_blob-12.29.0-py3-none-any.whl", hash = "sha256:ccf8a1bcd5e49df83ab85aab793b579e5ba2eeea2ad8900b2f62ca3a37dc391f", size = 434823, upload-time = "2026-05-15T03:35:01.837Z" }, -] - [[package]] name = "babel" version = "2.18.0" @@ -1345,7 +1290,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1962,15 +1907,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -3979,9 +3915,6 @@ dependencies = [ ] [package.optional-dependencies] -azure-storage = [ - { name = "azure-storage-blob" }, -] bigquery = [ { name = "google-cloud-bigquery" }, ] @@ -4167,7 +4100,6 @@ test = [ [package.metadata] requires-dist = [ { name = "adbc-driver-flightsql", marker = "extra == 'gizmosql'" }, - { name = "azure-storage-blob", marker = "extra == 'azure-storage'" }, { name = "docker" }, { name = "elasticsearch7", marker = "extra == 'elasticsearch7'" }, { name = "elasticsearch8", marker = "extra == 'elasticsearch8'" },