diff --git a/docker/client.py b/docker/client.py index 9f593d22..107fdff0 100644 --- a/docker/client.py +++ b/docker/client.py @@ -18,6 +18,7 @@ import re import shlex import struct import warnings +from datetime import datetime import requests import requests.exceptions @@ -30,6 +31,7 @@ from .utils import utils from . import errors from .tls import TLSConfig + if not six.PY3: import websocket @@ -290,7 +292,7 @@ class Client(requests.Session): return sock - def _stream_helper(self, response): + def _stream_helper(self, response, decode=False): """Generator for data coming from a chunked-encoded HTTP response.""" if response.raw._fp.chunked: reader = response.raw @@ -301,6 +303,8 @@ class Client(requests.Session): break if reader._fp.chunk_left: data += reader.read(reader._fp.chunk_left) + if decode: + data = json.loads(data) yield data else: # Response isn't chunked, meaning we probably @@ -565,8 +569,25 @@ class Client(requests.Session): return self._result(self._get(self._url("/containers/{0}/changes". format(container))), True) - def events(self): - return self._stream_helper(self.get(self._url('/events'), stream=True)) + def events(self, since=None, until=None, filters=None, decode=None): + if isinstance(since, datetime): + since = utils.datetime_to_timestamp(since) + + if isinstance(until, datetime): + until = utils.datetime_to_timestamp(until) + + if filters: + filters = utils.convert_filters(filters) + + params = { + 'since': since, + 'until': until, + 'filters': filters + } + + return self._stream_helper(self.get(self._url('/events'), + params=params, stream=True), + decode=decode) def execute(self, container, cmd, detach=False, stdout=True, stderr=True, stream=False, tty=False): diff --git a/docker/utils/utils.py b/docker/utils/utils.py index de2ecf59..3f51b312 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -20,6 +20,7 @@ import tarfile import tempfile from distutils.version import StrictVersion from fnmatch import fnmatch +from datetime import datetime import requests import six @@ -27,6 +28,7 @@ import six from .. import errors from .. import tls + DEFAULT_HTTP_HOST = "127.0.0.1" DEFAULT_UNIX_SOCKET = "http+unix://var/run/docker.sock" @@ -296,6 +298,12 @@ def convert_filters(filters): return json.dumps(result) +def datetime_to_timestamp(dt=datetime.now()): + """Convert a datetime in local timezone to a unix timestamp""" + delta = dt - datetime.fromtimestamp(0) + return delta.seconds + delta.days * 24 * 3600 + + def create_host_config( binds=None, port_bindings=None, lxc_conf=None, publish_all_ports=False, links=None, privileged=False, diff --git a/docs/api.md b/docs/api.md index 1d162e9f..d9eebc0c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -229,6 +229,28 @@ Inspect changes on a container's filesystem **Returns** (str): +## events + +Identical to the `docker events` command: get real time events from the server. The `events` +function return a blocking generator you can iterate over to retrieve events as they happen. + +**Params**: + +* since (datetime or int): get events from this point + +* until (datetime or int): get events until this point + +* filters (dict): filter the events by event time, container or image + +**Returns** (generator): + +```python +{u'status': u'start', + u'from': u'image/with:tag', + u'id': u'container-id', + u'time': 1423339459} +``` + ## execute ```python diff --git a/tests/fake_api.py b/tests/fake_api.py index d311fc11..ab2e4fd1 100644 --- a/tests/fake_api.py +++ b/tests/fake_api.py @@ -221,6 +221,13 @@ def get_fake_diff(): return status_code, response +def get_fake_events(): + status_code = 200 + response = [{'status': 'stop', 'id': FAKE_CONTAINER_ID, + 'from': FAKE_IMAGE_ID, 'time': 1423247867}] + return status_code, response + + def get_fake_export(): status_code = 200 response = 'Byte Stream....' @@ -402,5 +409,7 @@ fake_responses = { '{1}/{0}/containers/create'.format(CURRENT_VERSION, prefix): post_fake_create_container, '{1}/{0}/build'.format(CURRENT_VERSION, prefix): - post_fake_build_container + post_fake_build_container, + '{1}/{0}/events'.format(CURRENT_VERSION, prefix): + get_fake_events } diff --git a/tests/test.py b/tests/test.py index 5ad3ba93..bf7b3fbf 100644 --- a/tests/test.py +++ b/tests/test.py @@ -177,6 +177,57 @@ class DockerClientTest(Cleanup, unittest.TestCase): except Exception: pass + def test_events(self): + try: + self.client.events() + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + fake_request.assert_called_with( + url_prefix + 'events', + params={'since': None, 'until': None, 'filters': None}, + stream=True + ) + + def test_events_with_since_until(self): + ts = 1356048000 + now = datetime.datetime.fromtimestamp(ts) + since = now - datetime.timedelta(seconds=10) + until = now + datetime.timedelta(seconds=10) + try: + self.client.events(since=since, until=until) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + fake_request.assert_called_with( + url_prefix + 'events', + params={ + 'since': ts - 10, + 'until': ts + 10, + 'filters': None + }, + stream=True + ) + + def test_events_with_filters(self): + filters = {'event': ['die', 'stop'], + 'container': fake_api.FAKE_CONTAINER_ID} + try: + self.client.events(filters=filters) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + expected_filters = docker.utils.convert_filters(filters) + fake_request.assert_called_with( + url_prefix + 'events', + params={ + 'since': None, + 'until': None, + 'filters': expected_filters + }, + stream=True + ) + ################### # LISTING TESTS # ###################