diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c2ed7a4ec..a23509d70 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,7 @@ jobs: - google.py - kafka.py - localstack.py + - minio.py - mongodb.py - neo4j.py - nginx.py diff --git a/README.rst b/README.rst index c8a7aa3e9..488291985 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,7 @@ Currently available features: * RabbitMQ * Keycloak * Azurite container +* Minio container Installation ------------ diff --git a/docs/index.rst b/docs/index.rst index 8cdd90ce8..8404eb6da 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,4 +22,5 @@ Usage modes Keycloak container RabbitMQ container Redis container + Minio container diff --git a/docs/minio.rst b/docs/minio.rst new file mode 100644 index 000000000..d11916a3d --- /dev/null +++ b/docs/minio.rst @@ -0,0 +1,6 @@ +Minio +=================== + +Allows to spin up Minio Container. + +.. autoclass:: testcontainers.minio.MinioContainer diff --git a/requirements.in b/requirements.in index cd3bc8b87..f41b1b145 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,4 @@ --e file:.[docker-compose,mysql,oracle,postgresql,selenium,google-cloud-pubsub,mongo,redis,mssqlserver,neo4j,kafka,rabbitmq,clickhouse,keycloak,arangodb,azurite] +-e file:.[docker-compose,mysql,oracle,postgresql,selenium,google-cloud-pubsub,minio,mongo,redis,mssqlserver,neo4j,kafka,rabbitmq,clickhouse,keycloak,arangodb,azurite] codecov>=2.1.0 cryptography<37 flake8<3.8.0 # 3.8.0 adds a dependency on importlib-metadata which conflicts with other packages. diff --git a/requirements/3.10.txt b/requirements/3.10.txt index a35dc9ee3..bd62b9486 100644 --- a/requirements/3.10.txt +++ b/requirements/3.10.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.10 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: # # pip-compile --output-file=requirements/3.10.txt --resolver=backtracking requirements.in # @@ -38,6 +38,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.9.24 # via + # minio # msrest # requests # selenium @@ -47,7 +48,7 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests -clickhouse-driver==0.2.4 +clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 # via -r requirements.in @@ -95,9 +96,9 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in -google-api-core[grpc]==2.10.2 +google-api-core[grpc]==2.11.0 # via google-cloud-pubsub -google-auth==2.14.1 +google-auth==2.15.0 # via google-api-core google-cloud-pubsub==1.7.2 # via testcontainers @@ -110,7 +111,7 @@ greenlet==2.0.1 # via sqlalchemy grpc-google-iam-v1==0.12.4 # via google-cloud-pubsub -grpcio==1.50.0 +grpcio==1.51.1 # via # google-api-core # googleapis-common-protos @@ -126,7 +127,7 @@ idna==3.4 # trio imagesize==1.4.1 # via sphinx -importlib-metadata==5.0.0 +importlib-metadata==5.1.0 # via # keyring # twine @@ -152,11 +153,13 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +minio==7.1.12 + # via testcontainers more-itertools==9.0.0 # via jaraco-classes msrest==0.7.1 # via azure-storage-blob -neo4j==5.2.1 +neo4j==5.3.0 # via testcontainers oauthlib==3.2.2 # via requests-oauthlib @@ -175,7 +178,7 @@ pg8000==1.29.3 # via -r requirements.in pika==1.3.1 # via testcontainers -pkginfo==1.8.3 +pkginfo==1.9.2 # via twine pluggy==1.0.0 # via pytest @@ -227,7 +230,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.2 +python-arango==7.5.3 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -283,7 +286,7 @@ scramp==1.4.4 # via pg8000 secretstorage==3.3.3 # via keyring -selenium==4.6.0 +selenium==4.7.2 # via testcontainers six==1.16.0 # via @@ -292,7 +295,6 @@ six==1.16.0 # dockerpty # ecdsa # google-auth - # grpcio # isodate # jsonschema # paramiko @@ -332,17 +334,18 @@ trio==0.22.0 # trio-websocket trio-websocket==0.9.2 # via selenium -twine==4.0.1 +twine==4.0.2 # via -r requirements.in typing-extensions==4.4.0 # via azure-core -tzdata==2022.6 +tzdata==2022.7 # via pytz-deprecation-shim tzlocal==4.2 # via clickhouse-driver urllib3[socks]==1.26.13 # via # docker + # minio # python-arango # python-keycloak # requests @@ -358,7 +361,7 @@ wrapt==1.14.1 # via testcontainers wsproto==1.2.0 # via trio-websocket -zipp==3.10.0 +zipp==3.11.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/3.7.txt b/requirements/3.7.txt index 6252d8354..3db50f23f 100644 --- a/requirements/3.7.txt +++ b/requirements/3.7.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # pip-compile --output-file=requirements/3.7.txt --resolver=backtracking requirements.in # @@ -44,6 +44,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.9.24 # via + # minio # msrest # requests # selenium @@ -53,7 +54,7 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests -clickhouse-driver==0.2.4 +clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 # via -r requirements.in @@ -101,9 +102,9 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in -google-api-core[grpc]==2.10.2 +google-api-core[grpc]==2.11.0 # via google-cloud-pubsub -google-auth==2.14.1 +google-auth==2.15.0 # via google-api-core google-cloud-pubsub==1.7.2 # via testcontainers @@ -116,7 +117,7 @@ greenlet==2.0.1 # via sqlalchemy grpc-google-iam-v1==0.12.4 # via google-cloud-pubsub -grpcio==1.50.0 +grpcio==1.51.1 # via # google-api-core # googleapis-common-protos @@ -132,7 +133,7 @@ idna==3.4 # trio imagesize==1.4.1 # via sphinx -importlib-metadata==5.0.0 +importlib-metadata==5.1.0 # via # jsonschema # keyring @@ -166,11 +167,13 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +minio==7.1.12 + # via testcontainers more-itertools==9.0.0 # via jaraco-classes msrest==0.7.1 # via azure-storage-blob -neo4j==5.2.1 +neo4j==5.3.0 # via testcontainers oauthlib==3.2.2 # via requests-oauthlib @@ -189,7 +192,7 @@ pg8000==1.29.3 # via -r requirements.in pika==1.3.1 # via testcontainers -pkginfo==1.8.3 +pkginfo==1.9.2 # via twine pluggy==1.0.0 # via pytest @@ -241,7 +244,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.2 +python-arango==7.5.3 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -297,7 +300,7 @@ scramp==1.4.4 # via pg8000 secretstorage==3.3.3 # via keyring -selenium==4.6.0 +selenium==4.7.2 # via testcontainers six==1.16.0 # via @@ -306,7 +309,6 @@ six==1.16.0 # dockerpty # ecdsa # google-auth - # grpcio # isodate # jsonschema # paramiko @@ -346,7 +348,7 @@ trio==0.22.0 # trio-websocket trio-websocket==0.9.2 # via selenium -twine==4.0.1 +twine==4.0.2 # via -r requirements.in typing-extensions==4.4.0 # via @@ -356,13 +358,14 @@ typing-extensions==4.4.0 # importlib-metadata # redis # rich -tzdata==2022.6 +tzdata==2022.7 # via pytz-deprecation-shim tzlocal==4.2 # via clickhouse-driver urllib3[socks]==1.26.13 # via # docker + # minio # python-arango # python-keycloak # requests @@ -378,7 +381,7 @@ wrapt==1.14.1 # via testcontainers wsproto==1.2.0 # via trio-websocket -zipp==3.10.0 +zipp==3.11.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/3.8.txt b/requirements/3.8.txt index adc6ffac1..3b2b5c5bd 100644 --- a/requirements/3.8.txt +++ b/requirements/3.8.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: # # pip-compile --output-file=requirements/3.8.txt --resolver=backtracking requirements.in # @@ -42,6 +42,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.9.24 # via + # minio # msrest # requests # selenium @@ -51,7 +52,7 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests -clickhouse-driver==0.2.4 +clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 # via -r requirements.in @@ -99,9 +100,9 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in -google-api-core[grpc]==2.10.2 +google-api-core[grpc]==2.11.0 # via google-cloud-pubsub -google-auth==2.14.1 +google-auth==2.15.0 # via google-api-core google-cloud-pubsub==1.7.2 # via testcontainers @@ -114,7 +115,7 @@ greenlet==2.0.1 # via sqlalchemy grpc-google-iam-v1==0.12.4 # via google-cloud-pubsub -grpcio==1.50.0 +grpcio==1.51.1 # via # google-api-core # googleapis-common-protos @@ -130,7 +131,7 @@ idna==3.4 # trio imagesize==1.4.1 # via sphinx -importlib-metadata==5.0.0 +importlib-metadata==5.1.0 # via # keyring # sphinx @@ -157,11 +158,13 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +minio==7.1.12 + # via testcontainers more-itertools==9.0.0 # via jaraco-classes msrest==0.7.1 # via azure-storage-blob -neo4j==5.2.1 +neo4j==5.3.0 # via testcontainers oauthlib==3.2.2 # via requests-oauthlib @@ -180,7 +183,7 @@ pg8000==1.29.3 # via -r requirements.in pika==1.3.1 # via testcontainers -pkginfo==1.8.3 +pkginfo==1.9.2 # via twine pluggy==1.0.0 # via pytest @@ -232,7 +235,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.2 +python-arango==7.5.3 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -288,7 +291,7 @@ scramp==1.4.4 # via pg8000 secretstorage==3.3.3 # via keyring -selenium==4.6.0 +selenium==4.7.2 # via testcontainers six==1.16.0 # via @@ -297,7 +300,6 @@ six==1.16.0 # dockerpty # ecdsa # google-auth - # grpcio # isodate # jsonschema # paramiko @@ -337,19 +339,20 @@ trio==0.22.0 # trio-websocket trio-websocket==0.9.2 # via selenium -twine==4.0.1 +twine==4.0.2 # via -r requirements.in typing-extensions==4.4.0 # via # azure-core # rich -tzdata==2022.6 +tzdata==2022.7 # via pytz-deprecation-shim tzlocal==4.2 # via clickhouse-driver urllib3[socks]==1.26.13 # via # docker + # minio # python-arango # python-keycloak # requests @@ -365,7 +368,7 @@ wrapt==1.14.1 # via testcontainers wsproto==1.2.0 # via trio-websocket -zipp==3.10.0 +zipp==3.11.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/3.9.txt b/requirements/3.9.txt index e43224747..84d1a9ce3 100644 --- a/requirements/3.9.txt +++ b/requirements/3.9.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile --output-file=requirements/3.9.txt --resolver=backtracking requirements.in # @@ -38,6 +38,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.9.24 # via + # minio # msrest # requests # selenium @@ -47,7 +48,7 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests -clickhouse-driver==0.2.4 +clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 # via -r requirements.in @@ -95,9 +96,9 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in -google-api-core[grpc]==2.10.2 +google-api-core[grpc]==2.11.0 # via google-cloud-pubsub -google-auth==2.14.1 +google-auth==2.15.0 # via google-api-core google-cloud-pubsub==1.7.2 # via testcontainers @@ -110,7 +111,7 @@ greenlet==2.0.1 # via sqlalchemy grpc-google-iam-v1==0.12.4 # via google-cloud-pubsub -grpcio==1.50.0 +grpcio==1.51.1 # via # google-api-core # googleapis-common-protos @@ -126,7 +127,7 @@ idna==3.4 # trio imagesize==1.4.1 # via sphinx -importlib-metadata==5.0.0 +importlib-metadata==5.1.0 # via # keyring # sphinx @@ -153,11 +154,13 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +minio==7.1.12 + # via testcontainers more-itertools==9.0.0 # via jaraco-classes msrest==0.7.1 # via azure-storage-blob -neo4j==5.2.1 +neo4j==5.3.0 # via testcontainers oauthlib==3.2.2 # via requests-oauthlib @@ -176,7 +179,7 @@ pg8000==1.29.3 # via -r requirements.in pika==1.3.1 # via testcontainers -pkginfo==1.8.3 +pkginfo==1.9.2 # via twine pluggy==1.0.0 # via pytest @@ -228,7 +231,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.2 +python-arango==7.5.3 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -284,7 +287,7 @@ scramp==1.4.4 # via pg8000 secretstorage==3.3.3 # via keyring -selenium==4.6.0 +selenium==4.7.2 # via testcontainers six==1.16.0 # via @@ -293,7 +296,6 @@ six==1.16.0 # dockerpty # ecdsa # google-auth - # grpcio # isodate # jsonschema # paramiko @@ -333,17 +335,18 @@ trio==0.22.0 # trio-websocket trio-websocket==0.9.2 # via selenium -twine==4.0.1 +twine==4.0.2 # via -r requirements.in typing-extensions==4.4.0 # via azure-core -tzdata==2022.6 +tzdata==2022.7 # via pytz-deprecation-shim tzlocal==4.2 # via clickhouse-driver urllib3[socks]==1.26.13 # via # docker + # minio # python-arango # python-keycloak # requests @@ -359,7 +362,7 @@ wrapt==1.14.1 # via testcontainers wsproto==1.2.0 # via trio-websocket -zipp==3.10.0 +zipp==3.11.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/setup.py b/setup.py index 706333a2e..82bfe42d9 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,8 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Software Development :: Libraries :: Python Modules', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', @@ -59,6 +61,7 @@ 'postgresql': ['sqlalchemy', 'psycopg2-binary'], 'selenium': ['selenium'], 'google-cloud-pubsub': ['google-cloud-pubsub < 2'], + 'minio': ['minio'], 'mongo': ['pymongo'], 'redis': ['redis'], 'mssqlserver': ['pymssql'], diff --git a/testcontainers/minio.py b/testcontainers/minio.py new file mode 100644 index 000000000..039cff647 --- /dev/null +++ b/testcontainers/minio.py @@ -0,0 +1,111 @@ +from minio import Minio +from requests import ConnectionError, Response, get + +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready + + +class MinioContainer(DockerContainer): + """ + The example below spins up an Minio container and creates a new bucket in it. + Furthermore, it demonstrates how an object is written to this bucket and + then subsequently retrieved. + The method :code:`get_client` can be used to create a client for the Minio Python API. + The method :code:`get_config` can be used to retrieve the endpoint, access key + and secret key of the container. + + Example + ------- + .. doctest:: + + >>> import io + >>> from testcontainers.minio import MinioContainer + + >>> with MinioContainer() as minio: + ... client = minio.get_client() + ... client.make_bucket("test") + ... test_content = b"Hello World" + ... write_result = client.put_object( + ... "test", + ... "testfile.txt", + ... io.BytesIO(test_content), + ... length=len(test_content), + ... ) + ... retrieved_content = client.get_object("test", "testfile.txt").data + """ + + def __init__( + self, + image="minio/minio:RELEASE.2022-12-02T19-19-22Z", + port_to_expose=9000, + access_key="minioadmin", + secret_key="minioadmin", + **kwargs, + ): + """ + Args: + image (str, optional): The Docker image to use for the Minio container. + Defaults to "minio/minio:RELEASE.2022-12-02T19-19-22Z". + port_to_expose (int, optional): The port to expose on the container. + Defaults to 9000. + access_key (str, optional): The access key for client connections. + Defaults to "minioadmin". + secret_key (str, optional): The secret key for client connections. + Defaults to "minioadmin". + """ + super(MinioContainer, self).__init__(image, **kwargs) + self.port_to_expose = port_to_expose + self.access_key = access_key + self.secret_key = secret_key + + self.with_exposed_ports(self.port_to_expose) + self.with_env("MINIO_ACCESS_KEY", self.access_key) + self.with_env("MINIO_SECRET_KEY", self.secret_key) + self.with_command(f"server /data --address :{self.port_to_expose}") + + def get_client(self, **kwargs) -> Minio: + """Returns a Minio client to connect to the container. + + Returns: + Minio: Python Minio Client according to + https://min.io/docs/minio/linux/developers/python/API.html + """ + host_ip = self.get_container_host_ip() + exposed_port = self.get_exposed_port(self.port_to_expose) + return Minio( + f"{host_ip}:{exposed_port}", + access_key=self.access_key, + secret_key=self.secret_key, + secure=False, + **kwargs, + ) + + def get_config(self) -> dict: + """This method returns the configuration of the Minio container, + including the endpoint, access key, and secret key. + + Returns: + dict: {`endpoint`: str, `access_key`: str, `secret_key`: str} + """ + host_ip = self.get_container_host_ip() + exposed_port = self.get_exposed_port(self.port_to_expose) + return { + "endpoint": f"{host_ip}:{exposed_port}", + "access_key": self.access_key, + "secret_key": self.secret_key, + } + + @wait_container_is_ready(ConnectionError) + def _healthcheck(self): + """This is an internal method used to check if the Minio container + is healthy and ready to receive requests.""" + url = f"http://{self.get_config()['endpoint']}/minio/health/live" + response: Response = get(url) + response.raise_for_status() + + def start(self): + """This method starts the Minio container and runs the healthcheck + to verify that the container is ready to use.""" + super().start() + self._healthcheck() + return self diff --git a/tests/test_minio.py b/tests/test_minio.py new file mode 100644 index 000000000..99eed4380 --- /dev/null +++ b/tests/test_minio.py @@ -0,0 +1,21 @@ +import io + +from testcontainers.minio import MinioContainer + + +def test_docker_run_minio(): + config = MinioContainer(access_key="test-access", secret_key="test-secret") + with config as minio: + client = minio.get_client() + client.make_bucket("test") + test_content = b"Hello World" + client.put_object( + "test", + "testfile.txt", + io.BytesIO(test_content), + length=len(test_content), + ) + + assert client.get_object("test", "testfile.txt").data == test_content + assert minio.get_config()["access_key"] == config.access_key + assert minio.get_config()["secret_key"] == config.secret_key