From 6c6fbb315dc44d4c9f108d5d15834898dbea38a2 Mon Sep 17 00:00:00 2001 From: Doppular-Lab Date: Mon, 26 Oct 2026 15:44:08 -0700 Subject: [PATCH 1/2] feat(gcs): add fake-gcs-server container --- docs/community/gcs.rst | 2 + docs/modules/gcs.md | 23 ++++++ docs/modules/gcs_example.py | 30 +++++++ mkdocs.yml | 1 + pyproject.toml | 1 + src/testcontainers/community/gcs/__init__.py | 86 ++++++++++++++++++++ tests/community/gcs/test_gcs.py | 37 +++++++++ uv.lock | 75 ++++++++++++++++- 8 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 docs/community/gcs.rst create mode 100644 docs/modules/gcs.md create mode 100644 docs/modules/gcs_example.py create mode 100644 src/testcontainers/community/gcs/__init__.py create mode 100644 tests/community/gcs/test_gcs.py diff --git a/docs/community/gcs.rst b/docs/community/gcs.rst new file mode 100644 index 00000000..663170b7 --- /dev/null +++ b/docs/community/gcs.rst @@ -0,0 +1,2 @@ +.. autoclass:: testcontainers.community.gcs.GcsContainer +.. title:: testcontainers.community.gcs.GcsContainer diff --git a/docs/modules/gcs.md b/docs/modules/gcs.md new file mode 100644 index 00000000..dee26379 --- /dev/null +++ b/docs/modules/gcs.md @@ -0,0 +1,23 @@ +# Google Cloud Storage + +Since testcontainers-python :material-tag: v4.16.0 + +## Introduction + +The Testcontainers module for Google Cloud Storage, based on [fake-gcs-server](https://github.com/fsouza/fake-gcs-server). + +## Adding this module to your project dependencies + +Please run the following command to add the Google Cloud Storage module to your python dependencies: + +```bash +pip install testcontainers[gcs] +``` + +## Usage example + + + +[Creating a Google Cloud Storage container](gcs_example.py) + + diff --git a/docs/modules/gcs_example.py b/docs/modules/gcs_example.py new file mode 100644 index 00000000..3ca081bf --- /dev/null +++ b/docs/modules/gcs_example.py @@ -0,0 +1,30 @@ +from testcontainers.community.gcs import GcsContainer + + +def basic_example(): + with GcsContainer() as gcs: + # Get a client connected to the emulator + client = gcs.get_client() + + # Create a bucket + bucket = client.create_bucket("test-bucket") + print(f"Created bucket: {bucket.name}") + + # Upload objects + for i in range(1, 4): + blob = bucket.blob(f"test{i}.txt") + blob.upload_from_string(f"Hello from object {i}!") + print(f"Uploaded object: {blob.name}") + + # List objects + print("\nObjects in bucket:") + for blob in client.list_blobs(bucket.name): + print(f"Name: {blob.name}, Size: {blob.size} bytes") + + # Download an object + content = bucket.blob("test1.txt").download_as_text() + print(f"\nContent of test1.txt: {content}") + + +if __name__ == "__main__": + basic_example() diff --git a/mkdocs.yml b/mkdocs.yml index 761670cd..dbeda87e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,6 +77,7 @@ nav: - modules/weaviate.md - modules/aws.md - modules/azurite.md + - modules/gcs.md - modules/generic.md - modules/google.md - modules/k3s.md diff --git a/pyproject.toml b/pyproject.toml index 4ba4209e..320e05e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ db2 = [ "ibm_db_sa; platform_machine != 'aarch64' and platform_machine != 'arm64'", ] elasticsearch = [] +gcs = ["google-cloud-storage>=2"] generic = [ "httpx", "redis>=7", diff --git a/src/testcontainers/community/gcs/__init__.py b/src/testcontainers/community/gcs/__init__.py new file mode 100644 index 00000000..07b6da4e --- /dev/null +++ b/src/testcontainers/community/gcs/__init__.py @@ -0,0 +1,86 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import json +from urllib.request import Request, urlopen + +from google.auth.credentials import AnonymousCredentials +from google.cloud import storage +from typing_extensions import Self + +from testcontainers.core.container import DockerContainer +from testcontainers.core.wait_strategies import HttpWaitStrategy + +__all__ = ["GcsContainer"] + + +class GcsContainer(DockerContainer): + """ + Google Cloud Storage emulator container based on `fake-gcs-server `_. + + Example: + + .. doctest:: + + >>> from testcontainers.community.gcs import GcsContainer + + >>> with GcsContainer() as gcs: + ... client = gcs.get_client() + ... bucket = client.create_bucket("my-bucket") + ... bucket.blob("hello.txt").upload_from_string("Hello, GCS!") + ... content = bucket.blob("hello.txt").download_as_text() + >>> content + 'Hello, GCS!' + """ + + def __init__( + self, + image: str = "fsouza/fake-gcs-server:1.56.1", + port: int = 4443, + project: str = "test-project", + **kwargs: object, + ) -> None: + super().__init__(image=image, **kwargs) + self.port = port + self.project = project + self.with_exposed_ports(self.port) + self.with_command(f"-scheme http -port {self.port} -backend memory") + self.waiting_for(HttpWaitStrategy(self.port, "/storage/v1/b")) + + def get_url(self) -> str: + return f"http://{self.get_container_host_ip()}:{self.get_exposed_port(self.port)}" + + def start(self) -> Self: + super().start() + self._update_external_url() + return self + + def _update_external_url(self) -> None: + # fake-gcs-server builds object links (e.g. mediaLink) from its external URL, + # and the host-mapped port is only known once the container is running. + request = Request( + f"{self.get_url()}/_internal/config", + data=json.dumps({"externalUrl": self.get_url()}).encode(), + headers={"Content-Type": "application/json"}, + method="PUT", + ) + with urlopen(request, timeout=10): + pass + + def get_client(self, **kwargs: object) -> storage.Client: + """Get a ``google.cloud.storage.Client`` connected to the emulator.""" + return storage.Client( + project=self.project, + credentials=AnonymousCredentials(), + client_options={"api_endpoint": self.get_url()}, + **kwargs, + ) diff --git a/tests/community/gcs/test_gcs.py b/tests/community/gcs/test_gcs.py new file mode 100644 index 00000000..943c4ec2 --- /dev/null +++ b/tests/community/gcs/test_gcs.py @@ -0,0 +1,37 @@ +import os + +from testcontainers.community.gcs import GcsContainer + + +def test_gcs_upload_and_download(): + with GcsContainer() as gcs: + client = gcs.get_client() + bucket = client.create_bucket("test-bucket") + bucket.blob("hello.txt").upload_from_string("Hello, GCS!") + + assert bucket.blob("hello.txt").download_as_text() == "Hello, GCS!" + assert [blob.name for blob in client.list_blobs("test-bucket")] == ["hello.txt"] + + +def test_gcs_object_links_use_mapped_port(): + with GcsContainer() as gcs: + blob = gcs.get_client().create_bucket("test-bucket").blob("hello.txt") + blob.upload_from_string("Hello, GCS!") + blob.reload() + + assert blob.media_link.startswith(gcs.get_url()) + + +def test_gcs_resumable_upload(): + # larger than the client's 8 MiB multipart limit, so a resumable upload is used + data = os.urandom(10 * 1024 * 1024) + with GcsContainer() as gcs: + blob = gcs.get_client().create_bucket("test-bucket").blob("big.bin") + blob.upload_from_string(data) + + assert blob.download_as_bytes() == data + + +def test_gcs_custom_project(): + with GcsContainer(project="my-project") as gcs: + assert gcs.get_client().project == "my-project" diff --git a/uv.lock b/uv.lock index fe018a7c..65545612 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", @@ -1515,6 +1515,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/20/dd0b27d4ad4577c062e77ff968ca3e2d404186cd78c8a2a53a0ef5fe5389/google_cloud_pubsub-2.39.0-py3-none-any.whl", hash = "sha256:7210d691a46d7a66559696899ebe6eb731e63de29b624964b3be4dd2d12d3e19", size = 324665, upload-time = "2026-06-03T15:27:41.119Z" }, ] +[[package]] +name = "google-cloud-storage" +version = "3.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/d8/9d6444c1bc301bd4bad36cc91306524e8aba670cbefcc0d2fc184c88fbea/google_cloud_storage-3.17.1.tar.gz", hash = "sha256:b24df37900f5e0a93518bd7d3a2a7b7b48d8a1e72add8eb57d53a38b8b2b0640", size = 17355234, upload-time = "2026-10-15T17:49:57.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/77/4376bb26f24966008a9522c61bae025d7030baabf443f20c4b795fb58308/google_cloud_storage-3.17.1-py3-none-any.whl", hash = "sha256:287c7919d7bcda710eae6f6b60c756e89140475d9a4daec01b6174e737488ca8", size = 345706, upload-time = "2026-10-15T17:49:27.497Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/25/9cb0c1c31c45b893eb8f11ae70b3f4309432d59b5acaebca5dbe791729a4/google_crc32c-1.9.0.tar.gz", hash = "sha256:7b8c84c3d159ab6817fe3f74e6e6cef099c3f95dcec3abc0d8afb1404642efbe", size = 14857, upload-time = "2026-09-24T21:39:32.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/7c/e89a13c971bcab4a0464ecc78f8dc162c5c7ec8986a54dd867fc093b2c6f/google_crc32c-1.9.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e6b529a6a287104ec79d281c411685231200ce954a29c28ab8e5093cb6e130fb", size = 31959, upload-time = "2026-09-24T21:19:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/c6/06/510062c2acbdbf602d759b7b0086032c487126106bb25197b9da1ff1047f/google_crc32c-1.9.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:51cb4e23a38ad4f495f35f87c233ca3ea6b9c4559e7ac383cdef786fab0f7977", size = 31801, upload-time = "2026-09-24T21:22:24.222Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c6/53eaa12dc62625f4605760b09854a0814ef6afebdc0e611a38c7b15aa6d1/google_crc32c-1.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8535e75dfead304f30e9122b9ea2c0a570dbaa52c176a0a591540c7914c1e46d", size = 37203, upload-time = "2026-09-24T21:38:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/770c2713df471df73998f79758739da83e410ef576bfafd05e5e845959ff/google_crc32c-1.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:280f3a3e47af0eeba3a3e5aa7d311af77001812b8df80fb8beafcd0b40eaf7f1", size = 35816, upload-time = "2026-09-24T21:38:05.798Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f3/181945217690644aa502220a3ec9bf0d2ef9af930bbfffee555fe5236e2f/google_crc32c-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:56610f548f1b35c9568b9d1de30423480f505dae4991556072d5802820ff35c4", size = 35097, upload-time = "2026-09-24T21:39:27.402Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/a2f07f15e624f0de79359b1a6c1deb59ec5061bd3b38744b3b2849400662/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:457d0d9a4718fd52b1494eac5c200ad25beeadbdc91843d550a003910838589f", size = 31958, upload-time = "2026-09-24T21:19:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/923743597b774bbcf12a7c3e00e48d745e15fd616ad7489a40a63fff8f2f/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ccfe40021fd6afe23361175cf7551e3cef5fd34dc1ebe319f14993a83579e0eb", size = 31805, upload-time = "2026-09-24T21:22:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/df/a6/4d0352fe889663e0d81cea7fc664ec9158727384de4a44ab10e9967a7682/google_crc32c-1.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbef61a3794e011c65fb4396a196cf123a7f474fe5a443db8e5dd7d751b9e6d4", size = 38119, upload-time = "2026-09-24T21:38:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e3/26685384e4b66ff0928d9566ef6110a7df76029175a1842329d7e3515f10/google_crc32c-1.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86764b99e7a607830d93cb5b75e0ec3ff6cb06d3c274624418473cee701900d4", size = 36677, upload-time = "2026-09-24T21:38:08.082Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/4e90102e84880e97d3cf935f2672ecd29191bdeacf57f01740f92debda00/google_crc32c-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:43a2dc26f9be213fbe0b4fc4a1088c5d45cbfcb3247420ccc820f0fc3edeea86", size = 35096, upload-time = "2026-09-24T21:39:28.201Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/0730e1b3a14d054d1466f2fec88dadf978509c749a3d96d8b069cc56d38a/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53fdafef58e230d0c946ab5f8446d123d9f548230a73b29c8b41c9546f268bc1", size = 31961, upload-time = "2026-09-24T21:19:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/d085abaf2fd907121975b92245bb3480fb8be40c37d03f9d6c41857f84c3/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:8b91f41645b15a720357183fa5716682ada441873e3c462c15f9714be36f146b", size = 31804, upload-time = "2026-09-24T21:22:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/94/78/dd1935432337e5da7af391a6fc9f161c1c8e9b9002a402b9190135fe1b59/google_crc32c-1.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16865b477d7941712cb0e0aad8ad4815e984fb5fc16d3fdaef7d986e26e53c95", size = 37775, upload-time = "2026-09-24T21:38:09.249Z" }, + { url = "https://files.pythonhosted.org/packages/9e/43/9db03635bb10188d93dcbab9baa2a8670a0da4e868b4370cdbd98d65fed8/google_crc32c-1.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3abb18297d9ef0ab120531838be0e6d68c9fa876570e11c229c48f2edac23ce7", size = 36347, upload-time = "2026-09-24T21:38:10.141Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/94dee516c846bd9382c3f566d8f8e5fb9e90599e45afeb697f9fc2533528/google_crc32c-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb63a8d7fa2e95dcff1ca16af2f4d88b526fa5ff72d1696285884ac2d49b6963", size = 35097, upload-time = "2026-09-24T21:39:28.934Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/cb484e8b6174f130f8c6dc79c733a9dd8869b410ad6511fb6104c46b973a/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:f1dc17d987ddcc5eba12a7ce48f0eb93141dea236b170c1101151396edf2f0cf", size = 31960, upload-time = "2026-09-24T21:19:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/af/25/3e8e567bd48448e225ea27318ccf2b94e05124e7b8b97b13eaec9e127199/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f894a2877650b56201d26a012a257b76d54a68834dc3913a93830ca8a047b075", size = 31803, upload-time = "2026-09-24T21:22:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/f0/18/bee0dd59ae622482dc6463636c79e4bde7c954d061c859c9256362c9931a/google_crc32c-1.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4488f1553a9ab7e86cdedc833374a7e904031803b995dc0bd0be48c271fa6556", size = 37776, upload-time = "2026-09-24T21:38:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b6/e76e80fed5f2558273c7839e622f98095c9b36c719c7147e38e3c055cb70/google_crc32c-1.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0568b17ed90ac596f29400d99e243fd0cc6276766183def888d1bf8d1dc13827", size = 36350, upload-time = "2026-09-24T21:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/87/34/165542bfa99dfef91a76471cc48cce74b8ff4e295722896087ab2b8e8611/google_crc32c-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:8583ec21d56b565d68ab2963cc7e21b3b271247c29b04286068255ef65f221bd", size = 35090, upload-time = "2026-09-24T21:39:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/43ea41f4061a1cad87b2b6559c98e960e45bf551fe66f83d833b98aaf0c9/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6a3b2c8a343c570ed8100a7627c20badfd92c6caa2067093a86be45af27f5b1b", size = 31963, upload-time = "2026-09-24T21:19:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/45/d2/a968c0c29ccd2b0c980ff4f9e3f7035cee28c23a1c57541825cc8221858c/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:13179f7e3282617923e957b8e54b8f9c3968030f48640a9f47fd7c5c38c4a215", size = 31805, upload-time = "2026-09-24T21:22:27.917Z" }, + { url = "https://files.pythonhosted.org/packages/03/73/388e493d6c3e252e37165d22efe5a1361f872a24425391b999822861b23a/google_crc32c-1.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:265233aff33d835f5b909584fe36ab29647b598c271b661a300001099109e53e", size = 37925, upload-time = "2026-09-24T21:38:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/36/190d32caa363ef25d685f422ed1bbf93ff1140fb22fd4d90f24cec209977/google_crc32c-1.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dee799544cae42a42b17a88e38b59cf2c271051dc001da2117a8ff240ffa0548", size = 36495, upload-time = "2026-09-24T21:38:14.211Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fd/81cefea6adae7bd92abb23d4567d199f6485a20ec0a305ca5fa04c52b9c5/google_crc32c-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:af73200fa9791ccd380f3598235dba8d82b8af0905df045b3dc60b59836e8ddd", size = 35535, upload-time = "2026-09-24T21:39:30.52Z" }, + { url = "https://files.pythonhosted.org/packages/c5/18/19d4f17f3f33f8fdffcb3e1e69219d6f7ec2c359c160867b04dac1d0a64d/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e6e8be8a94436079cb5340f6d495d9d7ba30124d8b952703994c739c7c06e236", size = 31977, upload-time = "2026-09-24T21:19:03.976Z" }, + { url = "https://files.pythonhosted.org/packages/81/b4/8010372c4b46f2ee2352dfdb630c397570cd85522a315df024ad2f9459aa/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_x86_64.whl", hash = "sha256:f2b64641bca27497b986b9d87883014035aa904cb4fa333407c6752b3afee9ba", size = 31809, upload-time = "2026-09-24T21:22:29.1Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f8/7e33845d6b90ce1cf37cfabf25cb859277c7d3533ef1b6b1e1ca58581549/google_crc32c-1.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f97c3806dcea41c29c04965347b0e12481561b75e0045dc7a4f69d75dec5d9b1", size = 38359, upload-time = "2026-09-24T21:38:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/556b2423f449a7515af6b8222a4d7833cbe09ff3e8d2f0b80471f5f6d02e/google_crc32c-1.9.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0abe7e202c25909869c35672ab0f2fe748a7acf276eb78577332a7c38999740f", size = 36858, upload-time = "2026-09-24T21:38:15.799Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/4733f1b7c921d04a2bb9b9916cf66498bf7ad0860a06289413830da83192/google_crc32c-1.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:5695c8b9327e040b2aba12c6659b0acb5995314ef0af0192da66e662e011103b", size = 35534, upload-time = "2026-09-24T21:39:31.337Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/64/df6a482d5aa39d7f7be186d892377d605cae6c88525fd851d456e8bbe9c9/google_resumable_media-2.11.0.tar.gz", hash = "sha256:febd83686752799661b4de575f0b993c5c25c349a5362556fc4d7be164056a37", size = 2164914, upload-time = "2026-09-29T19:26:13.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/2e/4f0a152f2e576e496f31ba1c3c62ed174a8878d06008916a7edc58b1bb28/google_resumable_media-2.11.0-py3-none-any.whl", hash = "sha256:f43d15e6a7f818f762eaead0f369c551f8275a4179c9d6225d0d259f49b87b5d", size = 81515, upload-time = "2026-09-29T19:25:47.31Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.0" @@ -5487,6 +5554,9 @@ db2 = [ { name = "ibm-db-sa", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64'" }, { name = "sqlalchemy" }, ] +gcs = [ + { name = "google-cloud-storage" }, +] generic = [ { name = "httpx" }, { name = "redis" }, @@ -5671,6 +5741,7 @@ requires-dist = [ { name = "docker" }, { name = "google-cloud-datastore", marker = "extra == 'google'", specifier = ">=2" }, { name = "google-cloud-pubsub", marker = "extra == 'google'", specifier = ">=2" }, + { name = "google-cloud-storage", marker = "extra == 'gcs'", specifier = ">=2" }, { name = "httpx", marker = "extra == 'aws'" }, { name = "httpx", marker = "extra == 'cratedb'" }, { name = "httpx", marker = "extra == 'generic'" }, @@ -5711,7 +5782,7 @@ requires-dist = [ { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=4" }, { name = "wrapt" }, ] -provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "cratedb", "db2", "elasticsearch", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "toxiproxy", "valkey", "vault", "weaviate", "chroma", "trino"] +provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "cratedb", "db2", "elasticsearch", "gcs", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "toxiproxy", "valkey", "vault", "weaviate", "chroma", "trino"] [package.metadata.requires-dev] dev = [ From 5d95e5101ab14b8a43ea0d8a11f6b1f9ae0cf9fe Mon Sep 17 00:00:00 2001 From: Doppular-Lab Date: Mon, 26 Oct 2026 15:59:55 -0700 Subject: [PATCH 2/2] fix(gcs): make google-cloud-storage optional --- docs/modules/gcs.md | 2 +- pyproject.toml | 3 ++- src/testcontainers/community/gcs/__init__.py | 16 ++++++++++++---- uv.lock | 8 ++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/modules/gcs.md b/docs/modules/gcs.md index dee26379..ab9c5c34 100644 --- a/docs/modules/gcs.md +++ b/docs/modules/gcs.md @@ -11,7 +11,7 @@ The Testcontainers module for Google Cloud Storage, based on [fake-gcs-server](h Please run the following command to add the Google Cloud Storage module to your python dependencies: ```bash -pip install testcontainers[gcs] +pip install testcontainers[gcs] google-cloud-storage ``` ## Usage example diff --git a/pyproject.toml b/pyproject.toml index 320e05e7..a7eb486b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ db2 = [ "ibm_db_sa; platform_machine != 'aarch64' and platform_machine != 'arm64'", ] elasticsearch = [] -gcs = ["google-cloud-storage>=2"] +gcs = [] generic = [ "httpx", "redis>=7", @@ -136,6 +136,7 @@ test = [ "sqlalchemy-cockroachdb>=2", "sqlalchemy-cratedb", "paramiko>=4", + "google-cloud-storage>=2", "twine>=7.0.0", "anyio>=4", "tomli>=2.0; python_version < '3.11'", # required for pyproject.toml tests, TODO: remove once we drop py3.10 support diff --git a/src/testcontainers/community/gcs/__init__.py b/src/testcontainers/community/gcs/__init__.py index 07b6da4e..6714f13a 100644 --- a/src/testcontainers/community/gcs/__init__.py +++ b/src/testcontainers/community/gcs/__init__.py @@ -11,15 +11,17 @@ # License for the specific language governing permissions and limitations # under the License. import json +from typing import TYPE_CHECKING from urllib.request import Request, urlopen -from google.auth.credentials import AnonymousCredentials -from google.cloud import storage from typing_extensions import Self from testcontainers.core.container import DockerContainer from testcontainers.core.wait_strategies import HttpWaitStrategy +if TYPE_CHECKING: + from google.cloud import storage + __all__ = ["GcsContainer"] @@ -76,8 +78,14 @@ def _update_external_url(self) -> None: with urlopen(request, timeout=10): pass - def get_client(self, **kwargs: object) -> storage.Client: - """Get a ``google.cloud.storage.Client`` connected to the emulator.""" + def get_client(self, **kwargs: object) -> "storage.Client": + """Get a ``google.cloud.storage.Client`` connected to the emulator. + + Requires the ``google-cloud-storage`` package to be installed. + """ + from google.auth.credentials import AnonymousCredentials + from google.cloud import storage + return storage.Client( project=self.project, credentials=AnonymousCredentials(), diff --git a/uv.lock b/uv.lock index 65545612..f5eec0ad 100644 --- a/uv.lock +++ b/uv.lock @@ -5554,9 +5554,6 @@ db2 = [ { name = "ibm-db-sa", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64'" }, { name = "sqlalchemy" }, ] -gcs = [ - { name = "google-cloud-storage" }, -] generic = [ { name = "httpx" }, { name = "redis" }, @@ -5653,6 +5650,7 @@ dev = [ { name = "anyio" }, { name = "boto3-stubs-lite", extra = ["boto3"] }, { name = "cassandra-driver" }, + { name = "google-cloud-storage" }, { name = "hvac", marker = "python_full_version < '4'" }, { name = "kafka-python-ng" }, { name = "mkdocs" }, @@ -5706,6 +5704,7 @@ lint = [ test = [ { name = "anyio" }, { name = "cassandra-driver" }, + { name = "google-cloud-storage" }, { name = "hvac", marker = "python_full_version < '4'" }, { name = "kafka-python-ng" }, { name = "paho-mqtt" }, @@ -5741,7 +5740,6 @@ requires-dist = [ { name = "docker" }, { name = "google-cloud-datastore", marker = "extra == 'google'", specifier = ">=2" }, { name = "google-cloud-pubsub", marker = "extra == 'google'", specifier = ">=2" }, - { name = "google-cloud-storage", marker = "extra == 'gcs'", specifier = ">=2" }, { name = "httpx", marker = "extra == 'aws'" }, { name = "httpx", marker = "extra == 'cratedb'" }, { name = "httpx", marker = "extra == 'generic'" }, @@ -5789,6 +5787,7 @@ dev = [ { name = "anyio", specifier = ">=4" }, { name = "boto3-stubs-lite", extras = ["boto3"] }, { name = "cassandra-driver", specifier = ">=3.30" }, + { name = "google-cloud-storage", specifier = ">=2" }, { name = "hvac", marker = "python_full_version < '4'", specifier = ">=2" }, { name = "kafka-python-ng", specifier = ">=2" }, { name = "mkdocs", specifier = ">=1.5.3,<2.0.0" }, @@ -5840,6 +5839,7 @@ lint = [ test = [ { name = "anyio", specifier = ">=4" }, { name = "cassandra-driver", specifier = ">=3.30" }, + { name = "google-cloud-storage", specifier = ">=2" }, { name = "hvac", marker = "python_full_version < '4'", specifier = ">=2" }, { name = "kafka-python-ng", specifier = ">=2" }, { name = "paho-mqtt", specifier = ">=2" },