From dac8319e511ac623671328eb8283cc4416e18f09 Mon Sep 17 00:00:00 2001 From: Pepijn Date: Mon, 22 Aug 2022 11:23:42 +0200 Subject: [PATCH 1/4] Add azurite module --- .github/workflows/main.yml | 1 + README.rst | 1 + requirements.in | 2 +- requirements/3.10.txt | 23 ++++++++ requirements/3.6.txt | 40 +++++++++++++- requirements/3.7.txt | 22 ++++++++ requirements/3.8.txt | 23 ++++++++ requirements/3.9.txt | 23 ++++++++ setup.py | 1 + testcontainers/azurite.py | 110 +++++++++++++++++++++++++++++++++++++ tests/test_azurite.py | 13 +++++ 11 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 testcontainers/azurite.py create mode 100644 tests/test_azurite.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d749a65e2..46c13d042 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ jobs: - webdriver.py - keycloak.py - arangodb.py + - azurite.py runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 diff --git a/README.rst b/README.rst index ea6461f11..2f55e0567 100644 --- a/README.rst +++ b/README.rst @@ -26,6 +26,7 @@ Currently available features: * LocalStack * RabbitMQ * Keycloak +* Azurite container Installation ------------ diff --git a/requirements.in b/requirements.in index 4309c1d7f..90406ba20 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] +-e file:.[docker-compose,mysql,oracle,postgresql,selenium,google-cloud-pubsub,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 09d9c9faa..f2ee01781 100644 --- a/requirements/3.10.txt +++ b/requirements/3.10.txt @@ -22,6 +22,12 @@ attrs==21.4.0 # outcome # pytest # trio +azure-core==1.25.0 + # via + # azure-storage-blob + # msrest +azure-storage-blob==12.13.1 + # via testcontainers babel==2.10.1 # via sphinx bcrypt==3.2.2 @@ -30,6 +36,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.5.18.1 # via + # msrest # requests # urllib3 cffi==1.15.0 @@ -50,6 +57,7 @@ coverage[toml]==6.4.1 cryptography==36.0.2 # via # -r requirements.in + # azure-storage-blob # paramiko # pyopenssl # urllib3 @@ -113,6 +121,8 @@ imagesize==1.3.0 # via sphinx iniconfig==1.1.1 # via pytest +isodate==0.6.1 + # via msrest jinja2==3.1.2 # via sphinx jsonschema==3.2.0 @@ -123,8 +133,12 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +msrest==0.7.1 + # via azure-storage-blob neo4j==4.4.4 # via testcontainers +oauthlib==3.2.0 + # via requests-oauthlib outcome==1.1.0 # via trio packaging==21.3 @@ -210,14 +224,19 @@ redis==4.3.3 # via testcontainers requests==2.27.1 # via + # azure-core # codecov # docker # docker-compose # google-api-core + # msrest # python-arango # python-keycloak + # requests-oauthlib # requests-toolbelt # sphinx +requests-oauthlib==1.3.1 + # via msrest requests-toolbelt==0.9.1 # via python-arango rsa==4.8 @@ -230,10 +249,12 @@ selenium==4.2.0 # via testcontainers six==1.16.0 # via + # azure-core # dockerpty # ecdsa # google-auth # grpcio + # isodate # jsonschema # paramiko # websocket-client @@ -271,6 +292,8 @@ trio==0.20.0 # trio-websocket trio-websocket==0.9.2 # via selenium +typing-extensions==4.3.0 + # via azure-core tzdata==2022.1 # via pytz-deprecation-shim tzlocal==4.2 diff --git a/requirements/3.6.txt b/requirements/3.6.txt index 0acd6776f..d7ee5c3ff 100644 --- a/requirements/3.6.txt +++ b/requirements/3.6.txt @@ -16,6 +16,12 @@ attrs==21.4.0 # via # jsonschema # pytest +azure-core==1.24.2 + # via + # azure-storage-blob + # msrest +azure-storage-blob==12.13.1 + # via testcontainers babel==2.10.1 # via sphinx backports.zoneinfo==0.2.1 @@ -29,7 +35,9 @@ cached-property==1.5.2 cachetools==4.2.4 # via google-auth certifi==2021.10.8 - # via requests + # via + # msrest + # requests cffi==1.15.0 # via # bcrypt @@ -48,9 +56,12 @@ coverage[toml]==6.2 cryptography==36.0.2 # via # -r requirements.in + # azure-storage-blob # paramiko cx-oracle==8.3.0 # via testcontainers +dataclasses==0.8 + # via python-arango deprecated==1.2.13 # via redis deprecation==2.1.0 @@ -114,6 +125,8 @@ importlib-resources==5.4.0 # via backports.zoneinfo iniconfig==1.1.1 # via pytest +isodate==0.6.1 + # via msrest jinja2==3.0.3 # via sphinx jsonschema==3.2.0 @@ -124,13 +137,18 @@ markupsafe==2.0.1 # via jinja2 mccabe==0.6.1 # via flake8 +msrest==0.7.1 + # via azure-storage-blob neo4j==4.4.3 # via testcontainers +oauthlib==3.2.0 + # via requests-oauthlib packaging==21.3 # via # deprecation # pytest # redis + # setuptools-scm # sphinx paramiko==2.10.4 # via docker @@ -164,6 +182,8 @@ pyflakes==2.1.1 # via flake8 pygments==2.12.0 # via sphinx +pyjwt==2.4.0 + # via python-arango pymongo==4.1.1 # via testcontainers pymssql==2.2.5 @@ -182,6 +202,8 @@ pytest==7.0.1 # pytest-cov pytest-cov==3.0.0 # via -r requirements.in +python-arango==7.3.1 + # via testcontainers python-dotenv==0.20.0 # via docker-compose python-jose==3.3.0 @@ -201,12 +223,21 @@ redis==4.2.2 # via testcontainers requests==2.27.1 # via + # azure-core # codecov # docker # docker-compose # google-api-core + # msrest + # python-arango # python-keycloak + # requests-oauthlib + # requests-toolbelt # sphinx +requests-oauthlib==1.3.1 + # via msrest +requests-toolbelt==0.9.1 + # via python-arango rsa==4.8 # via # google-auth @@ -215,12 +246,16 @@ scramp==1.4.1 # via pg8000 selenium==3.141.0 # via testcontainers +setuptools-scm[toml]==6.4.2 + # via python-arango six==1.16.0 # via + # azure-core # dockerpty # ecdsa # google-auth # grpcio + # isodate # jsonschema # paramiko # websocket-client @@ -248,9 +283,11 @@ tomli==1.2.3 # via # coverage # pytest + # setuptools-scm typing-extensions==4.1.1 # via # async-timeout + # azure-core # importlib-metadata # redis tzdata==2022.1 @@ -259,6 +296,7 @@ tzlocal==4.2 # via clickhouse-driver urllib3==1.26.9 # via + # python-arango # requests # selenium websocket-client==0.59.0 diff --git a/requirements/3.7.txt b/requirements/3.7.txt index ebc2f128d..cadea609b 100644 --- a/requirements/3.7.txt +++ b/requirements/3.7.txt @@ -22,6 +22,12 @@ attrs==21.4.0 # outcome # pytest # trio +azure-core==1.25.0 + # via + # azure-storage-blob + # msrest +azure-storage-blob==12.13.1 + # via testcontainers babel==2.10.1 # via sphinx backports-zoneinfo==0.2.1 @@ -36,6 +42,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.5.18.1 # via + # msrest # requests # urllib3 cffi==1.15.0 @@ -56,6 +63,7 @@ coverage[toml]==6.4.1 cryptography==36.0.2 # via # -r requirements.in + # azure-storage-blob # paramiko # pyopenssl # urllib3 @@ -128,6 +136,8 @@ importlib-metadata==4.11.4 # sqlalchemy iniconfig==1.1.1 # via pytest +isodate==0.6.1 + # via msrest jinja2==3.1.2 # via sphinx jsonschema==3.2.0 @@ -138,8 +148,12 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +msrest==0.7.1 + # via azure-storage-blob neo4j==4.4.4 # via testcontainers +oauthlib==3.2.0 + # via requests-oauthlib outcome==1.1.0 # via trio packaging==21.3 @@ -225,14 +239,19 @@ redis==4.3.3 # via testcontainers requests==2.27.1 # via + # azure-core # codecov # docker # docker-compose # google-api-core + # msrest # python-arango # python-keycloak + # requests-oauthlib # requests-toolbelt # sphinx +requests-oauthlib==1.3.1 + # via msrest requests-toolbelt==0.9.1 # via python-arango rsa==4.8 @@ -245,10 +264,12 @@ selenium==4.2.0 # via testcontainers six==1.16.0 # via + # azure-core # dockerpty # ecdsa # google-auth # grpcio + # isodate # jsonschema # paramiko # websocket-client @@ -289,6 +310,7 @@ trio-websocket==0.9.2 typing-extensions==4.2.0 # via # async-timeout + # azure-core # h11 # importlib-metadata # redis diff --git a/requirements/3.8.txt b/requirements/3.8.txt index 823b0a9d2..4d9cd0942 100644 --- a/requirements/3.8.txt +++ b/requirements/3.8.txt @@ -22,6 +22,12 @@ attrs==21.4.0 # outcome # pytest # trio +azure-core==1.25.0 + # via + # azure-storage-blob + # msrest +azure-storage-blob==12.13.1 + # via testcontainers babel==2.10.1 # via sphinx backports-zoneinfo==0.2.1 @@ -34,6 +40,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.5.18.1 # via + # msrest # requests # urllib3 cffi==1.15.0 @@ -54,6 +61,7 @@ coverage[toml]==6.4.1 cryptography==36.0.2 # via # -r requirements.in + # azure-storage-blob # paramiko # pyopenssl # urllib3 @@ -119,6 +127,8 @@ importlib-metadata==4.11.4 # via sphinx iniconfig==1.1.1 # via pytest +isodate==0.6.1 + # via msrest jinja2==3.1.2 # via sphinx jsonschema==3.2.0 @@ -129,8 +139,12 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +msrest==0.7.1 + # via azure-storage-blob neo4j==4.4.4 # via testcontainers +oauthlib==3.2.0 + # via requests-oauthlib outcome==1.1.0 # via trio packaging==21.3 @@ -216,14 +230,19 @@ redis==4.3.3 # via testcontainers requests==2.27.1 # via + # azure-core # codecov # docker # docker-compose # google-api-core + # msrest # python-arango # python-keycloak + # requests-oauthlib # requests-toolbelt # sphinx +requests-oauthlib==1.3.1 + # via msrest requests-toolbelt==0.9.1 # via python-arango rsa==4.8 @@ -236,10 +255,12 @@ selenium==4.2.0 # via testcontainers six==1.16.0 # via + # azure-core # dockerpty # ecdsa # google-auth # grpcio + # isodate # jsonschema # paramiko # websocket-client @@ -277,6 +298,8 @@ trio==0.20.0 # trio-websocket trio-websocket==0.9.2 # via selenium +typing-extensions==4.3.0 + # via azure-core tzdata==2022.1 # via pytz-deprecation-shim tzlocal==4.2 diff --git a/requirements/3.9.txt b/requirements/3.9.txt index 8d9d6a15c..ac73ba552 100644 --- a/requirements/3.9.txt +++ b/requirements/3.9.txt @@ -22,6 +22,12 @@ attrs==21.4.0 # outcome # pytest # trio +azure-core==1.25.0 + # via + # azure-storage-blob + # msrest +azure-storage-blob==12.13.1 + # via testcontainers babel==2.10.1 # via sphinx bcrypt==3.2.2 @@ -30,6 +36,7 @@ cachetools==5.2.0 # via google-auth certifi==2022.5.18.1 # via + # msrest # requests # urllib3 cffi==1.15.0 @@ -50,6 +57,7 @@ coverage[toml]==6.4.1 cryptography==36.0.2 # via # -r requirements.in + # azure-storage-blob # paramiko # pyopenssl # urllib3 @@ -115,6 +123,8 @@ importlib-metadata==4.11.4 # via sphinx iniconfig==1.1.1 # via pytest +isodate==0.6.1 + # via msrest jinja2==3.1.2 # via sphinx jsonschema==3.2.0 @@ -125,8 +135,12 @@ markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 +msrest==0.7.1 + # via azure-storage-blob neo4j==4.4.4 # via testcontainers +oauthlib==3.2.0 + # via requests-oauthlib outcome==1.1.0 # via trio packaging==21.3 @@ -212,14 +226,19 @@ redis==4.3.3 # via testcontainers requests==2.27.1 # via + # azure-core # codecov # docker # docker-compose # google-api-core + # msrest # python-arango # python-keycloak + # requests-oauthlib # requests-toolbelt # sphinx +requests-oauthlib==1.3.1 + # via msrest requests-toolbelt==0.9.1 # via python-arango rsa==4.8 @@ -232,10 +251,12 @@ selenium==4.2.0 # via testcontainers six==1.16.0 # via + # azure-core # dockerpty # ecdsa # google-auth # grpcio + # isodate # jsonschema # paramiko # websocket-client @@ -273,6 +294,8 @@ trio==0.20.0 # trio-websocket trio-websocket==0.9.2 # via selenium +typing-extensions==4.3.0 + # via azure-core tzdata==2022.1 # via pytz-deprecation-shim tzlocal==4.2 diff --git a/setup.py b/setup.py index 5eb485455..fe8428993 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ 'clickhouse': ['clickhouse-driver'], 'keycloak': ['python-keycloak'], 'arangodb': ['python-arango'], + 'azurite': ['azure-storage-blob'], }, long_description_content_type="text/x-rst", long_description=long_description, diff --git a/testcontainers/azurite.py b/testcontainers/azurite.py new file mode 100644 index 000000000..7fed01741 --- /dev/null +++ b/testcontainers/azurite.py @@ -0,0 +1,110 @@ +# +# 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 os +import socket + +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready + + +class AzuriteContainer(DockerContainer): + """ + Azurite container. + + Example + ------- + :: + + with AzuriteContainer() as azurite: + connection_string = azurite.get_connection_string() + BlobServiceClient.from_connection_string( + connection_string, + api_version="2019-12-12" + ) + """ + + _AZURITE_ACCOUNT_NAME = os.environ.get("AZURITE_ACCOUNT_NAME", "devstoreaccount1") + _AZURITE_ACCOUNT_KEY = os.environ.get("AZURITE_ACCOUNT_KEY", "Eby8vdM02xNOcqFlqUwJPLlmEtlCDX" + "J1OUzFT50uSRZ6IFsuFq2UVErCz4I6" + "tq/K1SZFPTOtr/KBHBeksoGMGw==") + + _BLOB_SERVICE_PORT = 10_000 + _QUEUE_SERVICE_PORT = 10_001 + _TABLE_SERVICE_PORT = 10_002 + + def __init__( + self, + image="mcr.microsoft.com/azure-storage/azurite:latest", + ports_to_expose=None, + **kwargs + ): + """ Constructs an AzuriteContainer. + + Parameters + ---------- + image: str + Expects an image with tag. + ports_to_expose: List[int] + Expects a list with port numbers to expose. + kwargs + """ + super().__init__(image=image, **kwargs) + + if ports_to_expose is None: + ports_to_expose = [ + self._BLOB_SERVICE_PORT, + self._QUEUE_SERVICE_PORT, + self._TABLE_SERVICE_PORT + ] + + if len(ports_to_expose) == 0: + raise ValueError("Expected a list with port numbers to expose") + + self.ports_to_expose = ports_to_expose + + self.with_exposed_ports(*ports_to_expose) + self.with_env("AZURITE_ACCOUNTS", f"{self._AZURITE_ACCOUNT_NAME}:{self._AZURITE_ACCOUNT_KEY}") + + def get_connection_string(self): + host_ip = self.get_container_host_ip() + connection_string = f"DefaultEndpointsProtocol=http;" \ + f"AccountName={self._AZURITE_ACCOUNT_NAME};" \ + f"AccountKey={self._AZURITE_ACCOUNT_KEY};" + + if self._BLOB_SERVICE_PORT in self.ports_to_expose: + connection_string += f"BlobEndpoint=http://{host_ip}:" \ + f"{self.get_exposed_port(self._BLOB_SERVICE_PORT)}" \ + f"/{self._AZURITE_ACCOUNT_NAME};" + + if self._QUEUE_SERVICE_PORT in self.ports_to_expose: + connection_string += f"QueueEndpoint=http://{host_ip}:" \ + f"{self.get_exposed_port(self._QUEUE_SERVICE_PORT)}" \ + f"/{self._AZURITE_ACCOUNT_NAME};" + + if self._TABLE_SERVICE_PORT in self.ports_to_expose: + connection_string += f"TableEndpoint=http://{host_ip}:" \ + f"{self.get_exposed_port(self._TABLE_SERVICE_PORT)}" \ + f"/{self._AZURITE_ACCOUNT_NAME};" + + return connection_string + + def start(self): + super().start() + self._connect() + return self + + @wait_container_is_ready(OSError) + def _connect(self): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((self.get_container_host_ip(), + int(self.get_exposed_port(self.ports_to_expose[0])))) diff --git a/tests/test_azurite.py b/tests/test_azurite.py new file mode 100644 index 000000000..18b6cd3f5 --- /dev/null +++ b/tests/test_azurite.py @@ -0,0 +1,13 @@ +from testcontainers.azurite import AzuriteContainer +from azure.storage.blob import BlobServiceClient + + +def test_docker_run_azurite(): + with AzuriteContainer() as azurite_container: + blob_service_client = BlobServiceClient.from_connection_string( + azurite_container.get_connection_string(), + api_version="2019-12-12" + ) + + blob_service_client.create_container("test-container") + From 45586c6adcdb7b483300e44cfb3827bdaccff9c9 Mon Sep 17 00:00:00 2001 From: Pepijn Date: Wed, 24 Aug 2022 22:28:15 +0200 Subject: [PATCH 2/4] Applied autopep8 to new code --- tests/test_azurite.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_azurite.py b/tests/test_azurite.py index 18b6cd3f5..5c92e48ed 100644 --- a/tests/test_azurite.py +++ b/tests/test_azurite.py @@ -4,10 +4,9 @@ def test_docker_run_azurite(): with AzuriteContainer() as azurite_container: - blob_service_client = BlobServiceClient.from_connection_string( - azurite_container.get_connection_string(), - api_version="2019-12-12" - ) - - blob_service_client.create_container("test-container") + blob_service_client = BlobServiceClient.from_connection_string( + azurite_container.get_connection_string(), + api_version="2019-12-12" + ) + blob_service_client.create_container("test-container") From 86b696655a2475aa524cfeda2cf10930fe8743a2 Mon Sep 17 00:00:00 2001 From: Pepijn Date: Fri, 26 Aug 2022 11:32:48 +0200 Subject: [PATCH 3/4] fixed styling issues --- testcontainers/azurite.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/testcontainers/azurite.py b/testcontainers/azurite.py index 7fed01741..27acec87a 100644 --- a/testcontainers/azurite.py +++ b/testcontainers/azurite.py @@ -35,8 +35,8 @@ class AzuriteContainer(DockerContainer): _AZURITE_ACCOUNT_NAME = os.environ.get("AZURITE_ACCOUNT_NAME", "devstoreaccount1") _AZURITE_ACCOUNT_KEY = os.environ.get("AZURITE_ACCOUNT_KEY", "Eby8vdM02xNOcqFlqUwJPLlmEtlCDX" - "J1OUzFT50uSRZ6IFsuFq2UVErCz4I6" - "tq/K1SZFPTOtr/KBHBeksoGMGw==") + "J1OUzFT50uSRZ6IFsuFq2UVErCz4I6" + "tq/K1SZFPTOtr/KBHBeksoGMGw==") _BLOB_SERVICE_PORT = 10_000 _QUEUE_SERVICE_PORT = 10_001 @@ -73,7 +73,8 @@ def __init__( self.ports_to_expose = ports_to_expose self.with_exposed_ports(*ports_to_expose) - self.with_env("AZURITE_ACCOUNTS", f"{self._AZURITE_ACCOUNT_NAME}:{self._AZURITE_ACCOUNT_KEY}") + self.with_env("AZURITE_ACCOUNTS", + f"{self._AZURITE_ACCOUNT_NAME}:{self._AZURITE_ACCOUNT_KEY}") def get_connection_string(self): host_ip = self.get_container_host_ip() From 5eaf9be06719daab74a2d5d251d8e79bd5a9fb16 Mon Sep 17 00:00:00 2001 From: Pepijn Date: Tue, 30 Aug 2022 20:49:20 +0200 Subject: [PATCH 4/4] Removed ports_to_expose attribute and start using the ports attribute for azurite container logic --- testcontainers/azurite.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/testcontainers/azurite.py b/testcontainers/azurite.py index 27acec87a..748caadd0 100644 --- a/testcontainers/azurite.py +++ b/testcontainers/azurite.py @@ -70,8 +70,6 @@ def __init__( if len(ports_to_expose) == 0: raise ValueError("Expected a list with port numbers to expose") - self.ports_to_expose = ports_to_expose - self.with_exposed_ports(*ports_to_expose) self.with_env("AZURITE_ACCOUNTS", f"{self._AZURITE_ACCOUNT_NAME}:{self._AZURITE_ACCOUNT_KEY}") @@ -82,17 +80,17 @@ def get_connection_string(self): f"AccountName={self._AZURITE_ACCOUNT_NAME};" \ f"AccountKey={self._AZURITE_ACCOUNT_KEY};" - if self._BLOB_SERVICE_PORT in self.ports_to_expose: + if self._BLOB_SERVICE_PORT in self.ports: connection_string += f"BlobEndpoint=http://{host_ip}:" \ f"{self.get_exposed_port(self._BLOB_SERVICE_PORT)}" \ f"/{self._AZURITE_ACCOUNT_NAME};" - if self._QUEUE_SERVICE_PORT in self.ports_to_expose: + if self._QUEUE_SERVICE_PORT in self.ports: connection_string += f"QueueEndpoint=http://{host_ip}:" \ f"{self.get_exposed_port(self._QUEUE_SERVICE_PORT)}" \ f"/{self._AZURITE_ACCOUNT_NAME};" - if self._TABLE_SERVICE_PORT in self.ports_to_expose: + if self._TABLE_SERVICE_PORT in self.ports: connection_string += f"TableEndpoint=http://{host_ip}:" \ f"{self.get_exposed_port(self._TABLE_SERVICE_PORT)}" \ f"/{self._AZURITE_ACCOUNT_NAME};" @@ -108,4 +106,4 @@ def start(self): def _connect(self): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((self.get_container_host_ip(), - int(self.get_exposed_port(self.ports_to_expose[0])))) + int(self.get_exposed_port(next(iter(self.ports))))))