dockerctx 2026.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dockerctx/__init__.py ADDED
@@ -0,0 +1,199 @@
1
+ """ A context manager for a docker container. """
2
+ from __future__ import division, print_function
3
+
4
+
5
+ import socket
6
+ from contextlib import contextmanager
7
+ from importlib.metadata import PackageNotFoundError, version
8
+ import uuid
9
+ import logging
10
+ import time
11
+ import typing
12
+ import docker
13
+
14
+
15
+ try:
16
+ __version__ = version('dockerctx')
17
+ except PackageNotFoundError:
18
+ __version__ = '0.0.0'
19
+ __all__ = ['new_container']
20
+ logger = logging.getLogger('dockerctx')
21
+
22
+
23
+ @contextmanager
24
+ def new_container(
25
+ image_name,
26
+ new_container_name=lambda: uuid.uuid4().hex,
27
+ ports=None,
28
+ tmpfs=None,
29
+ ready_test=None,
30
+ docker_api_version='auto',
31
+ persist=lambda: False,
32
+ **kwargs):
33
+ """Start a docker container, and kill+remove when done.
34
+
35
+ :param new_container_name: The container name. By default, a UUID will be used.
36
+ If a callable, the result must be a str.
37
+ :type new_container_name: str | callable
38
+ :param ports: The list of port mappings to configure on the docker
39
+ container. The format is the same as that used in the `docker`
40
+ package, e.g. `ports={'5432/tcp': 60011}`
41
+ :type ports: typing.Dict[str, int]
42
+ :param tmpfs: When creating a container you can specify paths to be mounted
43
+ with tmpfs. It can be a list or a dictionary to configure on the docker
44
+ container. If it's a list, each item is a string specifying the path and
45
+ (optionally) any configuration for the mount, e.g. `tmpfs={'/mnt/vol2': '',
46
+ '/mnt/vol1': 'size=3G,uid=1000'}`
47
+ :type tmpfs: typing.Dict[str, str]
48
+ :param ready_test: A function to run to verify whether the container is "ready"
49
+ (in some sense) before yielding the container back to the caller. An example
50
+ of such a test is the `accepting_connections` function in the this module,
51
+ which will try repeatedly to connect to a socket, until either successfuly,
52
+ or a max timeout is reached. Use functools.partial to wrap up the args.
53
+ :type ready_test: typing.Callable[[], bool]
54
+ :param privileged: a privileged container is given access to all devices on
55
+ the host as well as set some configuration in AppArmor or SELinux to allow
56
+ the container nearly all the same access to the host as processes running
57
+ outside containers on the host.
58
+ :type persist: typing.Callable[[], bool]
59
+ :param persist: If True, the docker container will NOT be destroyed
60
+ during exit. This is sometimes useful if you want to keep a container
61
+ around, but only if tests fail. (That's why it's a callable: it only
62
+ gets called during the "exit" phase of the context manager).
63
+ :param kwargs: These extra keyword arguments will be passed through to the
64
+ `client.containers.run()` call. One of the more commons ones is to pass
65
+ a custom command through.
66
+
67
+ """
68
+ _ = new_container_name
69
+ name = str(_() if callable(_) else _)
70
+ client = docker.from_env(version=docker_api_version)
71
+
72
+ logger.info('New container: %s', name)
73
+ container = client.containers.run(
74
+ image_name, name=name, tmpfs=tmpfs, detach=True, ports=ports,
75
+ **kwargs
76
+ )
77
+ try:
78
+ logger.info('Waiting for container to be ready')
79
+ if ready_test and not ready_test():
80
+ raise ConnectionError(
81
+ 'Container {} not ready fast enough.'.format(name)
82
+ )
83
+ yield container
84
+ finally:
85
+ if persist():
86
+ logger.info('Leaving container up.')
87
+ else:
88
+ logger.info('Stopping container %s', name)
89
+ try:
90
+ container.stop(timeout=1)
91
+ except docker.errors.APIError as e:
92
+ logger.error('Error stopping container: %s', e)
93
+
94
+ logger.info('Removing container %s', name)
95
+ try:
96
+ container.remove(force=True)
97
+ except docker.errors.APIError as e:
98
+ logger.error('Error removing container: %s', e)
99
+
100
+
101
+ def accepting_connections(host, port, timeout=20):
102
+ """Try to make a socket connection to `(host, port)`
103
+
104
+ I'll try every 200 ms, and eventually give up after `timeout`.
105
+
106
+ :type host: str
107
+ :type port: int
108
+ :type timeout: int
109
+ :return: True for successful connection, False otherwise
110
+ :rtype: bool
111
+ """
112
+ t0 = time.time()
113
+ while time.time() - t0 < timeout:
114
+ try:
115
+ # s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
116
+ s = socket.create_connection((host, port))
117
+ logger.debug('Connected!')
118
+ s.close()
119
+ return True
120
+ except socket.error as ex:
121
+ logger.debug("Connection failed with errno %s: %s",
122
+ ex.errno, ex.strerror)
123
+ time.sleep(0.2)
124
+ return False
125
+
126
+
127
+ def pg_ready(host, port, dbuser='postgres', dbpass='password', dbname='postgres',
128
+ timeout=20, poll_freq=0.2):
129
+ """Wait until a postgres instance is ready to receive connections.
130
+
131
+ .. note::
132
+
133
+ This requires psycopg2 to be installed.
134
+
135
+ :type host: str
136
+ :type port: int
137
+ :type timeout: float
138
+ :type poll_freq: float
139
+ """
140
+ import psycopg2
141
+ t0 = time.time()
142
+ while time.time() - t0 < timeout:
143
+ try:
144
+ conn = psycopg2.connect(
145
+ "host={host} port={port} user={dbuser} password={dbpass} "
146
+ "dbname={dbname}".format(**vars())
147
+ )
148
+ logger.debug('Connected successfully.')
149
+ conn.close()
150
+ return True
151
+ except psycopg2.OperationalError as ex:
152
+ logger.debug("Connection failed: {0}".format(ex));
153
+ time.sleep(poll_freq)
154
+
155
+ logger.error('Postgres readiness check timed out.')
156
+ return False
157
+
158
+
159
+ @contextmanager
160
+ def session_scope(session_cls):
161
+ """Provide a transactional scope around a series of operations.
162
+
163
+ .. note::
164
+
165
+ This requires SQLAlchemy to be installed.
166
+
167
+ :type: sqlalchemy.orm.Session
168
+ """
169
+ session = session_cls()
170
+ try:
171
+ logger.debug('Yielding session')
172
+ yield session
173
+ logger.debug('Committing session')
174
+ session.commit()
175
+ except:
176
+ logger.exception('Error detected, rolling back session')
177
+ session.rollback()
178
+ raise
179
+ finally:
180
+ logger.debug('Closing session')
181
+ session.close()
182
+
183
+
184
+ def get_open_port():
185
+ """Return a currently-unused local network TCP port number
186
+
187
+ This is extremely handy when running unit tests because you may
188
+ not always be able to get the port of your choice, especially
189
+ in a continuous-integration context.
190
+
191
+ :return: TCP port number
192
+ :rtype: int
193
+ """
194
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
195
+ s.bind(('', 0)) # Using zero means the OS assigns one
196
+ address_info = s.getsockname()
197
+ port = int(address_info[1])
198
+ s.close()
199
+ return port
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: dockerctx
3
+ Version: 2026.1.1
4
+ Summary: A context manager for a docker container.
5
+ Author: Caleb Hattingh
6
+ Author-email: Caleb Hattingh <caleb.hattingh@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Natural Language :: English
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Dist: docker
20
+ Requires-Dist: psycopg2-binary
21
+ Requires-Python: >=3.10
22
+ Project-URL: Homepage, https://github.com/cjrh/dockerctx
23
+ Description-Content-Type: text/x-rst
24
+
25
+ .. image:: https://github.com/cjrh/dockerctx/workflows/Python%20application/badge.svg
26
+ :target: https://github.com/cjrh/dockerctx/actions
27
+
28
+ .. image:: https://coveralls.io/repos/github/cjrh/dockerctx/badge.svg?branch=master
29
+ :target: https://coveralls.io/github/cjrh/dockerctx?branch=master
30
+
31
+ .. image:: https://img.shields.io/pypi/pyversions/dockerctx.svg
32
+ :target: https://pypi.python.org/pypi/dockerctx
33
+
34
+ .. image:: https://img.shields.io/github/tag/cjrh/dockerctx.svg
35
+ :target: https://github.com/cjrh/dockerctx
36
+
37
+ .. image:: https://img.shields.io/pypi/v/dockerctx.svg
38
+ :target: https://img.shields.io/pypi/v/dockerctx.svg
39
+
40
+ dockerctx
41
+ =========
42
+
43
+ *dockerctx* is a context manager for managing the lifetime of a docker container.
44
+
45
+ The main use case is for setting up scaffolding for running tests, where you want
46
+ something a little broader than *unit tests*, but less heavily integrated than,
47
+ say, what you might write using `Robot framework`_.
48
+
49
+ .. _Robot framework: http://robotframework.org/
50
+
51
+ Install
52
+ -------
53
+
54
+ .. code-block:: bash
55
+
56
+ $ pip install dockerctx
57
+
58
+
59
+ The development-specific requirements will be installed automatically.
60
+
61
+ .. _flit: https://flit.readthedocs.io/en/latest/
62
+
63
+ Demo
64
+ ----
65
+
66
+ This is taken from one of the tests:
67
+
68
+ .. code-block:: python3
69
+
70
+ import time
71
+ import redis
72
+ import pytest
73
+ from dockerctx import new_container
74
+
75
+ # First make a pytest fixture
76
+
77
+ @pytest.fixture(scope='function')
78
+ def f_redis():
79
+
80
+ # This is the new thing! It's pretty clear. The `ready_test` provides
81
+ # a way to customize what "ready" means for each container. Here,
82
+ # we simply pause for a bit.
83
+
84
+ with new_container(
85
+ image_name='redis:latest',
86
+ ports={'6379/tcp': 56379},
87
+ ready_test=lambda: time.sleep(0.5) or True) as container:
88
+ yield container
89
+
90
+ # Here is the test. Since the fixture is at the "function" level, a fully
91
+ # new Redis container will be created for each test that uses this fixture.
92
+ # After the test completes, the container will be removed.
93
+
94
+ def test_redis_a(f_redis):
95
+ # The container object comes from the `docker` python package. Here we
96
+ # access only the "name" attribute, but there are many others.
97
+ print('Container %s' % f_redis.name)
98
+ r = redis.StrictRedis(host='localhost', port=56379, db=0)
99
+ r.set('foo', 'bar')
100
+ assert r.get('foo') == b'bar'
101
+
102
+ Note that a brand new Redis container is created here, used within the
103
+ context of the context manager (which is wrapped into a *pytest* fixture
104
+ here), and then the container is destroyed after the context manager
105
+ exits.
106
+
107
+
108
+ In the src, there is another, much more elaborate test which
109
+
110
+ #. runs a *postgres* container;
111
+ #. waits for postgres to begin accepting connections;
112
+ #. creates a database;
113
+ #. creates tables (using the SQLAlchemy_ ORM);
114
+ #. performs database operations;
115
+ #. tears down and removes the container afterwards.
116
+
117
+ .. _SQLAlchemy: http://www.sqlalchemy.org/
@@ -0,0 +1,5 @@
1
+ dockerctx/__init__.py,sha256=pH44R2OBJ1mga94ySwlQjyiCfUAg0YoGPlWLnBQkkqQ,6846
2
+ dockerctx-2026.1.1.dist-info/licenses/LICENSE,sha256=sMyqgCurQZSqNZRvfh_jQxYKkxjHekuekMaN0IdlrcU,1071
3
+ dockerctx-2026.1.1.dist-info/WHEEL,sha256=f5fWSvWsg5Knq5GWa6t1nJIug0Tqo69GqAWD_9LbBKw,81
4
+ dockerctx-2026.1.1.dist-info/METADATA,sha256=FRno6NpgEL-Ic98BSqE_d_DBnviVSvW5TkbsCH3wpII,3922
5
+ dockerctx-2026.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.16
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Caleb Hattingh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.