{ "info": { "author": "John Carr", "author_email": "john.carr@unrouted.co.uk", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Framework :: Pytest", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Topic :: Software Development :: Testing" ], "description": "# pytest-docker-tools\n\nYou have written a software application (in any language) and have packaged in as a Docker image. Now you want to smoke test the built image or do some integration testing with other containers before releasing it. You:\n\n * want to reason about your environment in a similar way to a `docker-compose.yml`\n * want the environment to be automatically created and destroyed as tests run\n * don't want to have to write loads of boilerplate code for creating the test environment\n * want to be able to run the tests in parallel\n * want the tests to be reliable\n\n`pytest-docker-tools` is a set of opinionated helpers for creating `py.test` fixtures for your smoke testing and integration testing needs. It strives to keep your environment definition declarative, like a docker-compose.yml. It embraces py.test fixture overloading. It tries not to be too magical.\n\nThe main interface provided by this library is a set of 'fixture factories'. It provides a 'best in class' implementation of a fixture, and then allows you to treat it as a template - injecting your own configuration declaratively. You can define your fixtures in your `conftest.py` and access them from all your tests, and you can override them as needed in individual test modules.\n\nThe API is straightforward and implicitly captures the interdependencies in the specification. For example, here is how it might look if you were building out a microservice and wanted to point its DNS and a mock DNS server:\n\n```python\n# conftest.py\n\nfrom http.client import HTTPConnection\n\nimport pytest\nfrom pytest_docker_tools import build, container\n\nfakedns_image = build(\n path='examples/resolver-service/dns',\n)\n\nfakedns = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '127.0.0.1',\n }\n)\n\napiserver_image = build(\n path='examples/resolver-service/api',\n)\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={\n '8080/tcp': None,\n },\n dns=['{fakedns.ips.primary}']\n)\n\n\n@pytest.fixture\ndef apiclient(apiserver):\n port = apiserver.ports['8080/tcp'][0]\n return HTTPConnection(f'localhost:{port}')\n```\n\nYou can now create a test that exercises your microservice:\n\n```python\n# test_smoketest.py\n\nimport socket\n\ndef test_my_frobulator(apiserver):\n sock = socket.socket()\n sock.connect(('127.0.0.1', apiserver.ports['8080/tcp'][0]))\n\n\ndef test_my_frobulator_works_after_restart(apiserver):\n apiserver.restart()\n\n sock = socket.socket()\n sock.connect(('127.0.0.1', apiserver.ports['8080/tcp'][0]))\n```\n\nIn this example all the dependencies will be resolved in order and once per session:\n\n * The latest `redis:latest` will be fetched\n * A container image will be build from the `Dockerfile` in the `db` folder.\n\nThen once per test:\n\n * A new volume will be created\n * A new 'backend' container will be created from `redis:latest`. It will be attached to the new volume.\n * A new 'frontend' container will be created from the freshly built container. It will be given the IP if the backend via an environment variable. Port 3679 in the container will be exposed as an ephemeral port on the host.\n\nThe test can then run and access the container via its ephemeral high port. At the end of the test the environment will be thrown away.\n\nIf the test fails the `docker logs` output from each container will be captured and added to the test output.\n\nIn the example you'll notice we defined an `apiclient` fixture. Of course if you use that it will implicitly pull in both of the server fixtures and 'just work':\n\n```python\n# test_smoketest.py\n\nimport json\n\n\ndef test_api_server(apiclient):\n apiclient.request('GET', '/')\n response = apiclient.getresponse()\n assert response.status == 200\n assert json.loads(response.read()) == {'result': '127.0.0.1'}\n```\n\n\n## Scope\n\nAll of the fixture factories take the `scope` keyword. Fixtures created with these factories will behave like any py.test fixture with that scope.\n\nIn this example we create a memcache that is `session` scoped and another that is `module` scoped.\n\n```python\n# conftest.py\n\nfrom pytest_docker_tools import container, fetch\n\nmemcache_image = fetch(repository='memcached:latest')\n\nmemcache_session = container(\n image='{memcache_image.id}',\n scope='session',\n ports={\n '11211/tcp': None,\n },\n)\n\nmemcache_module = container(\n image='{memcache_image.id}',\n scope='module',\n ports={\n '11211/tcp': None,\n },\n)\n```\n\nWhen `test_scope_1.py` runs neither container is running so a new instance of each is started. Their scope is longer than a single `function` so they are kept alive for the next test that needs them.\n\n```python\n# test_scope_1.py\n\nimport socket\n\ndef test_session_1(memcache_session):\n sock = socket.socket()\n sock.connect(('127.0.0.1', memcache_session.ports['11211/tcp'][0]))\n sock.sendall(b'set mykey 0 600 4\\r\\ndata\\r\\n')\n sock.close()\n\ndef test_session_2(memcache_session):\n sock = socket.socket()\n sock.connect(('127.0.0.1', memcache_session.ports['11211/tcp'][0]))\n sock.sendall(b'get mykey\\r\\n')\n assert sock.recv(1024) == b'VALUE mykey 0 4\\r\\ndata\\r\\nEND\\r\\n'\n sock.close()\n\ndef test_module_1(memcache_module):\n sock = socket.socket()\n sock.connect(('127.0.0.1', memcache_module.ports['11211/tcp'][0]))\n sock.sendall(b'set mykey 0 600 4\\r\\ndata\\r\\n')\n sock.close()\n\ndef test_module_2(memcache_module):\n sock = socket.socket()\n sock.connect(('127.0.0.1', memcache_module.ports['11211/tcp'][0]))\n sock.sendall(b'get mykey\\r\\n')\n assert sock.recv(1024) == b'VALUE mykey 0 4\\r\\ndata\\r\\nEND\\r\\n'\n sock.close()\n```\n\nWhen `test_scope_2.py` runs the `session` scoped container is still running, so it will be reused. But we are now in a new module now so the `module` scoped container will have been destroyed. A new empty instance will be created.\n\n```python\n# test_scope_2.py\n\nimport socket\n\ndef test_session_3(memcache_session):\n sock = socket.socket()\n sock.connect(('127.0.0.1', memcache_session.ports['11211/tcp'][0]))\n sock.sendall(b'get mykey\\r\\n')\n assert sock.recv(1024) == b'VALUE mykey 0 4\\r\\ndata\\r\\nEND\\r\\n'\n sock.close()\n\ndef test_module_3(memcache_module):\n sock = socket.socket()\n sock.connect(('127.0.0.1', memcache_module.ports['11211/tcp'][0]))\n sock.sendall(b'get mykey\\r\\n')\n assert sock.recv(1024) == b'END\\r\\n'\n sock.close()\n```\n\n\n## Parallelism\n\nIntegration and smoke tests are often slow, but a lot of time is spent waiting. So running tests in parallel is a great way to speed them up. `pytest-docker-tools` avoids creating resource names that could collide. It also makes it easy to not care what port your service is bound to. This means its a great fit for use with `pytest-xdist`.\n\nHere is a bare minimum example that just tests creating and destroying 100 instances of a redis fixture that runs under xdist. Create a `test_xdist.py` plugin:\n\n```python\n\nimport pytest\nfrom pytest_docker_tools import container, fetch\n\nmy_redis_image = fetch(repository='redis:latest')\n\nmy_redis = container(\n image='{my_redis_image.id}',\n)\n\n\n@pytest.mark.parametrize(\"i\", list(range(100)))\ndef test_xdist(i, my_redis):\n assert my_redis.status == \"running\"\n```\n\nAnd invoke it with:\n\n```bash\npytest test_xdist.py -n auto\n```\n\nIt will create a worker per core and run the tests in parallel:\n\n```\n===================================== test session starts ======================================\nplatform darwin -- Python 3.6.5, pytest-3.6.3, py-1.5.4, pluggy-0.6.0\nrootdir: ~/pytest-docker-tools, inifile:\nplugins: xdist-1.22.2, forked-0.2, docker-tools-0.0.2\ngw0 [100] / gw1 [100] / gw2 [100] / gw3 [100] / gw4 [100] / gw5 [100] / gw6 [100] / gw7 [100]\nscheduling tests via LoadScheduling\n......................................................................................... [ 82%]\n........... [100%]\n================================= 100 passed in 70.08 seconds ==================================\n```\n\n\n## Factories Reference\n\n### Containers\n\nTo create a container in your tests use the `container` fixture factory.\n\n```python\nfrom pytest_docker_tools import container\n\nmy_microservice_backend = container(image='redis:latest')\n```\n\nThe default scope for this factory is `function`. This means a new container will be created for each test.\n\nThe `container` fixture factory supports all parameters that can be passed to the docker-py `run` method. See [here](https://docker-py.readthedocs.io/en/stable/containers.html#docker.models.containers.ContainerCollection.run) for them all.\n\nAny string variables are interpolated against other defined fixtures. This means that a fixture can depend on other fixtures, and they will be built and run in order.\n\nFor example:\n\n```python\nfrom pytest_docker_tools import container, fetch\n\nredis_image = fetch(repository='redis:latest')\nredis = container(image='{redis_image.id}')\n\n\ndef test_container_starts(redis):\n assert redis.status == \"running\"\n```\n\nThis will fetch the latest `redis:latest` first, and then run a container from the exact image that was pulled. Note that if you don't use `build` or `fetch` to prepare a Docker image then the tag or hash that you specify must already exist on the host where you are running the tests. There is no implicit fetching of Docker images.\n\nThe container will be automatically deleted after the test has finished.\n\n\n#### Ip Addresses\n\nIf your container is only attached to a single network you can get its Ip address through a helper property. Given this environment:\n\n```python\n# conftest.py\n\nfrom pytest_docker_tools import container, fetch, network\n\nredis_image = fetch(repository='redis:latest')\nbackend_network = network()\n\nredis = container(\n image='{redis_image.id}',\n network='{backend_network.name}',\n)\n```\n\nYou can access the IP via the container helper:\n\n```python\nimport ipaddress\n\ndef test_get_service_ip(redis):\n # This will raise a ValueError if not a valid ip\n ipaddress.ip_address(redis.ips.primary)\n```\n\nIf you want to look up its ip address by network you can also access it more specifically:\n\n```python\nimport ipaddress\n\ndef test_get_service_ip(backend_network, redis):\n ipaddress.ip_address(redis.ips[backend_network])\n```\n\n#### Ports\n\nThe factory takes the same port arguments as the official Python Docker API. We recommend using the ephemeral high ports syntax:\n\n```python\n# conftest.py\n\nfrom pytest_docker_tools import container\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={'8080/tcp': None}\n)\n```\n\nDocker will map port 8080 in the container to a random port on your host. In order to access it from your tests you can get the bound port from the container instance:\n\n```python\ndef test_connect_my_service(apiserver):\n assert apiserver.ports['8080/tcp'][0] != 8080\n```\n\n\n#### Logs\n\nYou can inspect the logs of your container with the logs method:\n\n```python\nfrom pytest_docker_tools import container, fetch\n\n\nredis_image = fetch(repository='redis:latest')\nredis = container(image='{redis_image.id}')\n\ndef test_logs(redis):\n assert 'oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo' in redis.logs()\n```\n\n\n### Images\n\nTo pull an image from your default repository use the `fetch` fixture factory. To build an image from local source use the `build` fixture factory. If you are smoke testing an artifact already built locally you can use the `image` fixture factory to reference it.\n\n```python\nfrom pytest_docker_tools import build, image, fetch\n\nmy_image = fetch(repository='redis:latest')\n\nmy_image_2 = build(\n path='db'\n)\n```\n\nThe `build` fixture factory supports all parameters that can be passed to the docker-py `build` method. See [here](https://docker-py.readthedocs.io/en/stable/images.html#docker.models.images.ImageCollection.build) for them all. The `fetch` fixture factory supports all parameters that can be passed to the docker-py `pull` method. See [here](https://docker-py.readthedocs.io/en/stable/images.html#docker.models.images.ImageCollection.pull) for them all.\n\nThe default scope for this factory is `session`. This means the fixture will only build or fetch once per py.test invocation. The fixture will not be triggered until a test (or other fixture) tries to use it. This means you won't waste time building an image if you aren't running the test that uses it.\n\n\n### Networks\n\nBy default any containers you create with the `container()` fixture factory will run on your default docker network. You can create a dedicated network for your test with the `network()` fixture factory.\n\n```python\nfrom pytest_docker_tools import network\n\nfrontend_network = network()\n```\n\nThe `network` fixture factory supports all parameters that can be passed to the docker-py network `create` method. See [here](https://docker-py.readthedocs.io/en/stable/networks.html#docker.models.networks.NetworkCollection.create) for them all.\n\nThe default scope for this factory is `function`. This means a new network will be created for each test that is executed.\n\nThe network will be removed after the test using it has finished.\n\n\n### Volumes\n\nIn the ideal case a Docker container instance is read only. No data inside the container is written to, if it is its to a volume. If you are testing that your service can run read only you might want to mount a rw volume. You can use the `volume()` fixture factory to create a Docker volume with a lifecycle tied to your tests.\n\n```python\nfrom pytest_docker_tools import volume\n\nbackend_storage = volume()\n```\n\nThe `volume` fixture factory supports all parameters that can be passed to the docker-py volume `create` method. See [here](https://docker-py.readthedocs.io/en/stable/volumes.html#docker.models.volumes.VolumeCollection.create) for them all.\n\nIn addition you can specify a `initial_content` dictionary. This allows you to seed a volume with a small set of initial state. In the following example we'll preseed a minio service with 2 buckets and 1 object in 1 of those buckets.\n\n```python\nfrom pytest_docker_tools import container, fetch, volume\n\n\nminio_image = fetch(repository='minio/minio:latest')\n\nminio_volume = volume(\n initial_content={\n 'bucket-1': None,\n 'bucket-2/example.txt': b'Test file 1',\n }\n)\n\nminio = container(\n image='{minio_image.id}',\n command=['server', '/data'],\n volumes={\n '{minio_volume.name}': {'bind': '/data'},\n },\n environment={\n 'MINIO_ACCESS_KEY': 'minio',\n 'MINIO_SECRET_KEY': 'minio123',\n },\n)\n\ndef test_volume_is_seeded(minio):\n files = minio.get_files('/data')\n assert files['data/bucket-2/example.txt'] == b'Test file 1'\n assert files['data/bucket-1'] == None\n```\n\nThe `minio_volume` container will be created with an empty folder (`bucket-1`) and a text file called `example.txt` in a seperate folder called `bucket-2`.\n\nThe default scope for this factory is `function`. This means a new volume will be created for each test that is executed. The volume will be removed after the test using it has finished.\n\n\n## Fixtures\n\n### docker_client\n\nThe `docker_client` fixture returns an instance of the official docker client.\n\n```python\ndef test_container_created(docker_client, fakedns):\n for c in docker_client.containers.list(ignore_removed=True):\n if c.id == fakedns.id:\n # Looks like we managed to start one!\n break\n else:\n assert False, 'Looks like we failed to start a container'\n```\n\nTake care when using the `docker_client` directly:\n\n * Obviously resources created imperatively via the API won't be removed at the end of the test automatically\n * It's easy to break xdist compatibility\n * Always use `ignore_removed` with `docker_client.containers.list()` - it is racy without\n * It's easy to find other instances of the resources you are working with (created in other workers). Be mindful of this!\n * Don't take destructive action - someone could be running tests on a machine with other (non-test) containers running, collateral damage is easy and should be avoided.\n\nThis is the fixture used by our fixture factories. This means if you define a `docker_client` fixture of your own then the tests will use that instead.\n\n\n## Tips and tricks\n\n### Testing build artifacts\n\nWe often find ourselves using a set of tests against a container we've built at test time (with `build()`) but then wanting to use the same tests with an artifact generated on our CI platform (with `image()`). This ended up looking like this:\n\n```\nif not os.environ.get('IMAGE_ID', ''):\n image = build(path='examples/resolver-service/dns')\nelse:\n image = image(name=os.environ['IMAGE_ID'])\n```\n\nBut now you can just do:\n\n```python\nfrom pytest_docker_tools import image_or_build\n\nimage = image_or_build(\n environ_key='IMAGE_ID',\n path='examples/resolver-service/dns',\n)\n\ndef test_image(image):\n assert image.attrs['Os'] == 'linux'\n```\n\n\n### Network differences between dev env and CI\n\nAnother common difference between your dev environment and your CI environment might be that your tests end up running in Docker on your CI. If you bind-mount your `docker.sock` then your tests might end up running on the same container network as the containers you are testing, and unable to access any port you are mapping to the host box. In otherwords:\n\n * On your dev machine your tests might access locahost:8000 to access your test instance (ports mapped to host)\n * On your CI machine they might need to access 172.16.0.5:8000 to access your test instance\n\nThe container object has a `get_addr` helper which will return the right thing depending on the environment it is in.\n\n```python\nfrom pytest_docker_tools import container\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={'8080/tcp': None}\n)\n\ndef test_connect_my_service(apiserver):\n ip, port = apiserver.get_addr('8080/tcp')\n # ... connect to ip:port ...\n```\n\n\n### Client fixtures\n\nYou will probably want to create an API client for the service you are testing. Although we've already done this in the README, its worth calling it out. You can define a client fixture, have it depend on your docker containers, and then only have to reference the client from your tests.\n\n```python\n# conftest.py\n\nfrom http.client import HTTPConnection\n\nimport pytest\nfrom pytest_docker_tools import build, container\n\nfakedns_image = build(\n path='examples/resolver-service/dns',\n)\n\nfakedns = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '127.0.0.1',\n }\n)\n\napiserver_image = build(\n path='examples/resolver-service/api',\n)\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={\n '8080/tcp': None,\n },\n dns=['{fakedns.ips.primary}']\n)\n\n\n@pytest.fixture\ndef apiclient(apiserver):\n port = apiserver.ports['8080/tcp'][0]\n return HTTPConnection(f'localhost:{port}')\n```\n\nAnd then reference it from your tests:\n\n```python\n# test_the_test_client.py\n\nimport json\n\n\ndef test_api_server(apiclient):\n apiclient.request('GET', '/')\n response = apiclient.getresponse()\n assert response.status == 200\n result = json.loads(response.read())\n assert result['result'] == '127.0.0.1'\n```\n\nIn this example, any test that uses the `hpfeeds_client` fixture will get a properly configure client connected to a broker running in a Docker container on an ephemeral high port. When the test finishes the client will cleanly disconnect, and the docker container will be thrown away.\n\n\n### Fixture overloading\n\nComplicated environments can be defined with fixture factories. They form a directed acyclic graph. By using fixture overloading it is possible to (in the context of a single test module) replace a node in that dependency graph without having to redefine the entire environment.\n\n#### Replacing a container fixture without having to redefine its dependents\n\nYou can define a fixture in your `conftest.py`:\n\n```python\n# conftest.py\n\nfrom http.client import HTTPConnection\n\nimport pytest\nfrom pytest_docker_tools import build, container\n\nfakedns_image = build(\n path='examples/resolver-service/dns',\n)\n\nfakedns = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '127.0.0.1',\n }\n)\n\napiserver_image = build(\n path='examples/resolver-service/api',\n)\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={\n '8080/tcp': None,\n },\n dns=['{fakedns.ips.primary}']\n)\n\n\n@pytest.fixture\ndef apiclient(apiserver):\n port = apiserver.ports['8080/tcp'][0]\n return HTTPConnection(f'localhost:{port}')\n```\n\nYou can then overload these fixtures in your test modules. For example, if redis had a magic replication feature and you want to test for an edge case with your API you could in your `test_smoketest_alternate.py`:\n\n```python\n# test_smoketest_alternate.py\n\nimport json\n\nfrom pytest_docker_tools import container\n\nfakedns = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '192.168.192.168',\n }\n)\n\ndef test_api_server(apiclient):\n apiclient.request('GET', '/')\n response = apiclient.getresponse()\n assert response.status == 200\n result = json.loads(response.read())\n assert result['result'] == '192.168.192.168'\n```\n\nHere we have redefined the fakedns container locally in `test_smoketest_alternate`. It is able to use the `fakedns_image` fixture we defined in `conftest.py`. More crucially though, in `test_smoketest_alternate.py` when we use the core `apiclient` fixture it actually pulls in the local definition of `fakedns` and not the one from `conftest.py`! You don't have to redefine anything else. It just works.\n\n\n#### Injecting fixture configuration through fixtures\n\nYou can pull in normal py.test fixtures from your fixture factory too. This means we can use fixture overloading and pass in config. In your `conftest.py`:\n\n```python\n# conftest.py\n\nfrom http.client import HTTPConnection\n\nimport pytest\nfrom pytest_docker_tools import build, container\n\nfakedns_image = build(\n path='examples/resolver-service/dns',\n)\n\nfakedns = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '{example_com_a}',\n }\n)\n\napiserver_image = build(\n path='examples/resolver-service/api',\n)\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={\n '8080/tcp': None,\n },\n dns=['{fakedns.ips.primary}']\n)\n\n\n@pytest.fixture\ndef apiclient(apiserver):\n port = apiserver.ports['8080/tcp'][0]\n return HTTPConnection(f'localhost:{port}')\n\n\n@pytest.fixture\ndef example_com_a():\n return '127.0.0.1'\n\n```\n\nWhen a test uses the apiclient fixture now they will get the fakedns container configured as normal. However you can redefine the fixture in your test module - and the other fixtures will still respect it. For example:\n\n```python\n# test_smoketest_alternate.py\n\nimport json\n\nimport pytest\n\n\n@pytest.fixture\ndef example_com_a():\n return '192.168.192.168'\n\n\ndef test_api_server(apiclient):\n apiclient.request('GET', '/')\n response = apiclient.getresponse()\n assert response.status == 200\n result = json.loads(response.read())\n assert result['result'] == '192.168.192.168'\n```\n\nYour `api_server` container (and its `redis` backend) will be built as normal, only in this one test module it will use its sqlite backend.\n\n\n### Fixture parameterisation\n\nYou can create parameterisation fixtures. Perhaps you wan to run all your `api_server` tests against both of your authentication backends. Perhaps you have a fake that you want to test multiple configurations of.\n\nIn your `conftest.py`:\n\n```python\n# conftest.py\n\nfrom http.client import HTTPConnection\n\nimport pytest\nfrom pytest_docker_tools import build, container\n\nfakedns_image = build(\n path='examples/resolver-service/dns',\n)\n\nfakedns_localhost = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '127.0.0.1',\n }\n)\n\nfakedns_alternate = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '192.168.192.168',\n }\n)\n\n@pytest.fixture(scope='function', params=['fakedns_localhost', 'fakedns_alternate'])\ndef fakedns(request):\n return request.getfixturevalue(request.param)\n\napiserver_image = build(\n path='examples/resolver-service/api',\n)\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={\n '8080/tcp': None,\n },\n dns=['{fakedns.ips.primary}']\n)\n\n\n@pytest.fixture\ndef apiclient(apiserver):\n port = apiserver.ports['8080/tcp'][0]\n return HTTPConnection(f'localhost:{port}')\n```\n\nThe test is the same as the first example, only now it will be tested against 2 different fake configurations.\n\n```python\n# test_smoketest.py\n\nimport ipaddress\nimport json\n\n\ndef test_api_server(apiclient):\n apiclient.request('GET', '/')\n response = apiclient.getresponse()\n assert response.status == 200\n result = json.loads(response.read())\n ipaddress.ip_address(result['result'])\n```\n\nThis test will be invoked twice - once against the memory backend, and once against the sqlite backend.\n\n\n### Fixture wrappers\n\nYou can wrap your fixtures with a `wrapper_class`. This allows you to add helper methods to fixtures for use in your tests. In the case of the `container` fixture factory you can also implement `ready()` to add additional container readyness checks.\n\nIn previous tests we've created an entire test client fixture. With `wrapper_class` we could hang this convenience method off the fixture itself instead:\n\n```python\n# test_fixture_wrappers.py\n\nimport ipaddress\nimport json\nimport random\n\nfrom http.client import HTTPConnection\n\nimport pytest\nfrom pytest_docker_tools import build, container\nfrom pytest_docker_tools import wrappers\n\n\nclass Container(wrappers.Container):\n\n def ready(self):\n # This is called until it returns True - its a great hook for e.g.\n # waiting until a log message appears or a pid file is created etc\n if super().ready():\n return random.choice([True, False])\n return False\n\n def client(self):\n port = self.ports['8080/tcp'][0]\n return HTTPConnection(f'localhost:{port}')\n\n\nfakedns_image = build(\n path='examples/resolver-service/dns',\n)\n\nfakedns = container(\n image='{fakedns_image.id}',\n environment={\n 'DNS_EXAMPLE_COM__A': '127.0.0.1',\n }\n)\n\napiserver_image = build(\n path='examples/resolver-service/api',\n)\n\napiserver = container(\n image='{apiserver_image.id}',\n ports={\n '8080/tcp': None,\n },\n dns=['{fakedns.ips.primary}'],\n wrapper_class=Container,\n)\n\n\ndef test_container_wrapper_class(apiserver):\n client = apiserver.client()\n client.request('GET', '/')\n response = client.getresponse()\n assert response.status == 200\n result = json.loads(response.read())\n ipaddress.ip_address(result['result'])\n\n```\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/Jc2k/pytest-docker-tools", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "pytest_docker_tools", "package_url": "https://pypi.org/project/pytest_docker_tools/", "platform": "", "project_url": "https://pypi.org/project/pytest_docker_tools/", "project_urls": { "Homepage": "https://github.com/Jc2k/pytest-docker-tools" }, "release_url": "https://pypi.org/project/pytest_docker_tools/0.2.0/", "requires_dist": [ "pytest", "docker" ], "requires_python": ">=3.6", "summary": "An opionated set of helpers for defining Docker integration test environments with py.test fixtures.", "version": "0.2.0" }, "last_serial": 4654055, "releases": { "0.0.1": [ { "comment_text": "", "digests": { "md5": "6e9f642a5944a86fc1e1f0cb00284273", "sha256": "e143b4c04b37afb2d2f25138b515ffa4aa337a41c65aa5c0e7f51d303ff175e1" }, "downloads": -1, "filename": "pytest-docker-tools-0.0.1.tar.gz", "has_sig": false, "md5_digest": "6e9f642a5944a86fc1e1f0cb00284273", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 5709, "upload_time": "2018-07-22T15:52:44", "url": "https://files.pythonhosted.org/packages/bc/31/b7dc05094062a6c989c0e2005a74f061b0cc0f5f9c218e5e6ba6659d44dd/pytest-docker-tools-0.0.1.tar.gz" } ], "0.0.10": [ { "comment_text": "", "digests": { "md5": "293e0be73230cc527597294f7a1ea4f4", "sha256": "1f43fdbfc497c61cd6da8ffe9f0bd3d0243d609ac4226531fa4830134c075b5d" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.10-py3-none-any.whl", "has_sig": false, "md5_digest": "293e0be73230cc527597294f7a1ea4f4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 27585, "upload_time": "2018-08-07T13:26:27", "url": "https://files.pythonhosted.org/packages/01/60/2a23fe2b635a8756698c4668cedaf307cc6c924fd6015501d3514bae6c65/pytest_docker_tools-0.0.10-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3840bde36a6a6269382378152fe10923", "sha256": "771635b800410faf1c8c9d7bcd41e8d52569f85ddb5afb91ed9585394a8605b8" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.10.tar.gz", "has_sig": false, "md5_digest": "3840bde36a6a6269382378152fe10923", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 16106, "upload_time": "2018-08-07T13:26:32", "url": "https://files.pythonhosted.org/packages/e2/49/50a9a9c28481cfa31ad108054869e316698b54fb310784f945d3aa6a8a8a/pytest_docker_tools-0.0.10.tar.gz" } ], "0.0.11": [ { "comment_text": "", "digests": { "md5": "2dd1cf1588465fa4346c336fda39e7e8", "sha256": "d17da50b615f796ef64d381a62feeab99cccbdfa5394f6d5b846471bfd443534" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.11-py3-none-any.whl", "has_sig": false, "md5_digest": "2dd1cf1588465fa4346c336fda39e7e8", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 28641, "upload_time": "2018-08-28T11:50:35", "url": "https://files.pythonhosted.org/packages/0b/98/00d771dc271ace6d0875dd3a38f4688a8fb69efaef15fddbede0dece8e1c/pytest_docker_tools-0.0.11-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f26fc970d161c9566b71603a74625b7a", "sha256": "aad92f1dba75f1f3600b8a216b20816f7f531375ddb7a680a686b524dcb2faf0" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.11.tar.gz", "has_sig": false, "md5_digest": "f26fc970d161c9566b71603a74625b7a", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 16382, "upload_time": "2018-08-28T11:50:36", "url": "https://files.pythonhosted.org/packages/5f/2a/5bbe6d8804d8ec126205e79b7b04b19807af1f7d1c5080d741a4e7a384f1/pytest_docker_tools-0.0.11.tar.gz" } ], "0.0.12": [ { "comment_text": "", "digests": { "md5": "73a1ede6ebb6dfa143a9980589adbc7e", "sha256": "ef59a316e946baadfacf60ec57a2b2a00bb8109fbc5e10199224272fff5b0f43" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.12-py3-none-any.whl", "has_sig": false, "md5_digest": "73a1ede6ebb6dfa143a9980589adbc7e", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 31711, "upload_time": "2018-11-06T13:38:43", "url": "https://files.pythonhosted.org/packages/fd/f0/8fdee6c920dcd15c77ac582fa69a1577791497130b78291ed71867e57408/pytest_docker_tools-0.0.12-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "4d9dbef0d3f56d42e40738e4bd37bc65", "sha256": "0929273284d111280a3db271c3a9378459fea2318a71839d3e2b4f9a4923a372" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.12.tar.gz", "has_sig": false, "md5_digest": "4d9dbef0d3f56d42e40738e4bd37bc65", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 17709, "upload_time": "2018-11-06T13:38:45", "url": "https://files.pythonhosted.org/packages/4b/80/373c1c7cd545afc1190a26581bb2ab06207425dd29c4e3bba411f14fab6b/pytest_docker_tools-0.0.12.tar.gz" } ], "0.0.2": [ { "comment_text": "", "digests": { "md5": "1b234d01fc0620c9eff76f8f009548b5", "sha256": "dbbcedc816c0e0ccd3bf71e45d7ee1b3b1ed87922b6662c15c5d3eb6700fae65" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.2-py3-none-any.whl", "has_sig": false, "md5_digest": "1b234d01fc0620c9eff76f8f009548b5", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 18572, "upload_time": "2018-07-22T21:34:04", "url": "https://files.pythonhosted.org/packages/d3/ae/734f02703fb2b4d4d1ad5046664e52329235cac1601424e41964745bcee9/pytest_docker_tools-0.0.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "09b5498f007726767af8aea8d63b9e89", "sha256": "3e94e1eaaacf71eaa1419e6fd58bad4cae0a292315f3d79ab4f738444ac0d1da" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.2.tar.gz", "has_sig": false, "md5_digest": "09b5498f007726767af8aea8d63b9e89", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 7401, "upload_time": "2018-07-22T21:34:06", "url": "https://files.pythonhosted.org/packages/20/84/5e2e1c776fa13025145efa74c444bd05c3b4661715debd463eb6ab5cb9e3/pytest_docker_tools-0.0.2.tar.gz" } ], "0.0.3": [ { "comment_text": "", "digests": { "md5": "6a0e86d8364b2bef2d2383f84dcf0db9", "sha256": "34465245a225afc8b5421d1a70a5123e69a94e2a3516af7ce90597170f35c071" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.3-py3-none-any.whl", "has_sig": false, "md5_digest": "6a0e86d8364b2bef2d2383f84dcf0db9", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 20902, "upload_time": "2018-07-24T10:22:21", "url": "https://files.pythonhosted.org/packages/2d/34/d069d352a0e17b4d47ba5d9e098852b249fae8999136e53c5a2a1adc1b05/pytest_docker_tools-0.0.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f8c0d825016048213891584de44610ab", "sha256": "b2f6935408fd01bc6a4aae62612be5fc5f5825c00d8cb04ec1a7025b3de34c2c" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.3.tar.gz", "has_sig": false, "md5_digest": "f8c0d825016048213891584de44610ab", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 11531, "upload_time": "2018-07-24T10:22:36", "url": "https://files.pythonhosted.org/packages/6c/7a/1bfe5984896ba304abae48381382cc0f57c5f10561a74c25874dd8a61217/pytest_docker_tools-0.0.3.tar.gz" } ], "0.0.4": [ { "comment_text": "", "digests": { "md5": "210698fc6bf6682df0875a4a53fa1184", "sha256": "b183d0013645a8ccbf5f851f31bd7b4e99f479ebb8b096f2668c7a5fa71f1ff1" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.4-py3-none-any.whl", "has_sig": false, "md5_digest": "210698fc6bf6682df0875a4a53fa1184", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 21556, "upload_time": "2018-07-26T17:07:36", "url": "https://files.pythonhosted.org/packages/d8/3d/4018b6057d4917d42a45d0886cbd266c08950df8c1150ce60347c3215ee0/pytest_docker_tools-0.0.4-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5bd26b0702f52c1e01d5b210f84f2af6", "sha256": "050baaf7d67be77c65f18cead0505b01e184c18ae4bb4b92996db03887e0132b" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.4.tar.gz", "has_sig": false, "md5_digest": "5bd26b0702f52c1e01d5b210f84f2af6", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 14045, "upload_time": "2018-07-26T17:07:38", "url": "https://files.pythonhosted.org/packages/95/9a/4e78e51bbb861523c38a160e53c7b016af5d42eeae77fe880342656268bc/pytest_docker_tools-0.0.4.tar.gz" } ], "0.0.5": [ { "comment_text": "", "digests": { "md5": "f8ed9e4e3b6fb3c1b6c0d707f463d935", "sha256": "389c20f126ca8c895382133c822bf6798166358613bbf1388d2ab5db3065060a" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.5-py3-none-any.whl", "has_sig": false, "md5_digest": "f8ed9e4e3b6fb3c1b6c0d707f463d935", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 24173, "upload_time": "2018-07-27T14:03:29", "url": "https://files.pythonhosted.org/packages/d2/55/bdda6722ff842b6372812da0da7f56ab41572324142dfc36bb04a58ca5fb/pytest_docker_tools-0.0.5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "158c117d2b27b75fa729a667fd7dda91", "sha256": "39d40f7c83af04f40ed917b6ac2d387400b65e2c391524f3e65dad974ed63cae" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.5.tar.gz", "has_sig": false, "md5_digest": "158c117d2b27b75fa729a667fd7dda91", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 14300, "upload_time": "2018-07-27T14:03:33", "url": "https://files.pythonhosted.org/packages/db/68/6c67dc188af071769c0958d6221dd12041f1c52a39610a77ce2e5886e0e8/pytest_docker_tools-0.0.5.tar.gz" } ], "0.0.6": [ { "comment_text": "", "digests": { "md5": "1754ac308618f56c80d96e19c1e861f1", "sha256": "5d9c3afb6c30f66ca8ad08aa1436d820adce89f3bdfae55e350b0a9d0e5376c6" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.6-py3-none-any.whl", "has_sig": false, "md5_digest": "1754ac308618f56c80d96e19c1e861f1", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 23990, "upload_time": "2018-07-30T10:14:19", "url": "https://files.pythonhosted.org/packages/fd/5f/e5e0eb444b4c2898e9b6e2e48bbf74d21bffe324b8f95359d5488b8faa9c/pytest_docker_tools-0.0.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f444b9117b7118417e5a1921769a1505", "sha256": "ce8cb4ed56d95c2b43abba2229c1f73a6fcd0293bdf89c48462a72d7b8274353" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.6.tar.gz", "has_sig": false, "md5_digest": "f444b9117b7118417e5a1921769a1505", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 14294, "upload_time": "2018-07-30T10:14:23", "url": "https://files.pythonhosted.org/packages/e1/e9/dbadf94b234f64e3808a07547e952bee55f8653b24450b62d232a0b1ef78/pytest_docker_tools-0.0.6.tar.gz" } ], "0.0.7": [ { "comment_text": "", "digests": { "md5": "a824242d1bff7e0e55b3d1dfdf800d8d", "sha256": "d1de685546a06bbec6182eaa4a925e85242a08069f2afc263130c76f38374c75" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.7-py3-none-any.whl", "has_sig": false, "md5_digest": "a824242d1bff7e0e55b3d1dfdf800d8d", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 26188, "upload_time": "2018-07-30T17:28:37", "url": "https://files.pythonhosted.org/packages/8f/ce/56250909862e22d2cb797ea1b86e26aab6cc9e46b2fd45eef680a20acb03/pytest_docker_tools-0.0.7-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "949b3b00214165aacffe5124428f8a23", "sha256": "aec0cf7d86b53da2be3ce866a07e0514c982882c858b73021f6fb9dfddeb2322" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.7.tar.gz", "has_sig": false, "md5_digest": "949b3b00214165aacffe5124428f8a23", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 15141, "upload_time": "2018-07-30T17:28:39", "url": "https://files.pythonhosted.org/packages/e6/bf/b7c0e4a6e7b01f58ecf4a1543bceccb72ce062b6be2361c42a430d6579cf/pytest_docker_tools-0.0.7.tar.gz" } ], "0.0.8": [ { "comment_text": "", "digests": { "md5": "41e24f8f7a24784e65ba55afd04fcb80", "sha256": "10a7e4b27d86d33d5c41c024401c9eee885f75136584ae63afe555d1d2d6baa1" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.8-py3-none-any.whl", "has_sig": false, "md5_digest": "41e24f8f7a24784e65ba55afd04fcb80", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 26156, "upload_time": "2018-07-31T13:20:16", "url": "https://files.pythonhosted.org/packages/03/8e/471a93dd4ca33b11a7793697a4cb4f1644d7e1814259a4055e9a841e170e/pytest_docker_tools-0.0.8-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "fd7d0b17ae2be70c6c8592b4940a374c", "sha256": "e7a4d5b5374720ea8e7d0af36ecfc4f93b9f3f09546476d5cd9fdd5bc3694741" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.8.tar.gz", "has_sig": false, "md5_digest": "fd7d0b17ae2be70c6c8592b4940a374c", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 15187, "upload_time": "2018-07-31T13:20:20", "url": "https://files.pythonhosted.org/packages/b1/a7/b726a92f732d436532bc2fc935f7fd8af56ea9c7e435349804321fd3100d/pytest_docker_tools-0.0.8.tar.gz" } ], "0.0.9": [ { "comment_text": "", "digests": { "md5": "08c4eed3b0b35eee7c6c10810c8b2f9c", "sha256": "979d876ef57f6725c92f481494e85c12925adda97252d613ba622ab35abac8d3" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.9-py3-none-any.whl", "has_sig": false, "md5_digest": "08c4eed3b0b35eee7c6c10810c8b2f9c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 27556, "upload_time": "2018-08-07T12:12:46", "url": "https://files.pythonhosted.org/packages/2c/8f/1547fab6c58ba8e69d64e3298d22cc6bbcfddb52d993f257a883063bbca1/pytest_docker_tools-0.0.9-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ab907484e0aea2c106a1fff511d997dd", "sha256": "0a79cf63d7adfa0f3eeb055f190ac06df21ff10a36db611d35f44c66c9c991cb" }, "downloads": -1, "filename": "pytest_docker_tools-0.0.9.tar.gz", "has_sig": false, "md5_digest": "ab907484e0aea2c106a1fff511d997dd", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 16101, "upload_time": "2018-08-07T12:12:51", "url": "https://files.pythonhosted.org/packages/81/13/b18318e51682d9664c30af5d619336e93d3c1fd66217598786b107e28e49/pytest_docker_tools-0.0.9.tar.gz" } ], "0.1.0": [ { "comment_text": "", "digests": { "md5": "2fa4a4e04911e714246c1ef4d5e32b93", "sha256": "79a592e9e922d6079b59b4d3624d40e23b274af3b48f4f83dc49e5d105ca6b38" }, "downloads": -1, "filename": "pytest_docker_tools-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "2fa4a4e04911e714246c1ef4d5e32b93", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 31694, "upload_time": "2018-11-06T13:50:02", "url": "https://files.pythonhosted.org/packages/42/96/2132e9ea1b2a8adf941cdf8fe12b7b5b78b0190121d9405174f62fd40202/pytest_docker_tools-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "15443ca67f6ba2cb4215d8e07efcf112", "sha256": "427fd17e85323e43848dc0208cf80830d26f2072db23bedb09ce78cf9615e7c9" }, "downloads": -1, "filename": "pytest_docker_tools-0.1.0.tar.gz", "has_sig": false, "md5_digest": "15443ca67f6ba2cb4215d8e07efcf112", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 17698, "upload_time": "2018-11-06T13:50:04", "url": "https://files.pythonhosted.org/packages/7d/e9/e25ce859ce5c42d60d031ab831e93f98acd41e3773d10eeed2fae8a0fc07/pytest_docker_tools-0.1.0.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "c033972068f4f0b49aae1b83190b9248", "sha256": "637951a14e81190dfdf31ea91b41773c673e9b3ab6da89da30d5faac076209e7" }, "downloads": -1, "filename": "pytest_docker_tools-0.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "c033972068f4f0b49aae1b83190b9248", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 31793, "upload_time": "2019-01-02T20:48:11", "url": "https://files.pythonhosted.org/packages/b4/58/f18d63cc886f7a4049290589b7d9f53170f3a776dfef98ca946db0423f43/pytest_docker_tools-0.2.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ffbef3a38e431552fad7a1d08ab26e83", "sha256": "68ad707e7f41664d69dab4452f5491e6cc62f443d648ae5c0914d5e074a55b84" }, "downloads": -1, "filename": "pytest_docker_tools-0.2.0.tar.gz", "has_sig": false, "md5_digest": "ffbef3a38e431552fad7a1d08ab26e83", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 18123, "upload_time": "2019-01-02T20:48:14", "url": "https://files.pythonhosted.org/packages/60/88/aea62b37db7ba0ec94841dc5066dbc51b600f67a0b02dbbe155323d002bf/pytest_docker_tools-0.2.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "c033972068f4f0b49aae1b83190b9248", "sha256": "637951a14e81190dfdf31ea91b41773c673e9b3ab6da89da30d5faac076209e7" }, "downloads": -1, "filename": "pytest_docker_tools-0.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "c033972068f4f0b49aae1b83190b9248", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 31793, "upload_time": "2019-01-02T20:48:11", "url": "https://files.pythonhosted.org/packages/b4/58/f18d63cc886f7a4049290589b7d9f53170f3a776dfef98ca946db0423f43/pytest_docker_tools-0.2.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ffbef3a38e431552fad7a1d08ab26e83", "sha256": "68ad707e7f41664d69dab4452f5491e6cc62f443d648ae5c0914d5e074a55b84" }, "downloads": -1, "filename": "pytest_docker_tools-0.2.0.tar.gz", "has_sig": false, "md5_digest": "ffbef3a38e431552fad7a1d08ab26e83", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 18123, "upload_time": "2019-01-02T20:48:14", "url": "https://files.pythonhosted.org/packages/60/88/aea62b37db7ba0ec94841dc5066dbc51b600f67a0b02dbbe155323d002bf/pytest_docker_tools-0.2.0.tar.gz" } ] }