-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pinching a few json stream utils from Compose. Signed-off-by: Ben Firshman <ben@firshman.co.uk>
- Loading branch information
Showing
5 changed files
with
187 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
from __future__ import absolute_import | ||
from __future__ import unicode_literals | ||
|
||
import json | ||
import json.decoder | ||
|
||
import six | ||
|
||
from ..errors import StreamParseError | ||
|
||
|
||
json_decoder = json.JSONDecoder() | ||
|
||
|
||
def stream_as_text(stream): | ||
"""Given a stream of bytes or text, if any of the items in the stream | ||
are bytes convert them to text. | ||
This function can be removed once docker-py returns text streams instead | ||
of byte streams. | ||
""" | ||
for data in stream: | ||
if not isinstance(data, six.text_type): | ||
data = data.decode('utf-8', 'replace') | ||
yield data | ||
|
||
|
||
def json_splitter(buffer): | ||
"""Attempt to parse a json object from a buffer. If there is at least one | ||
object, return it and the rest of the buffer, otherwise return None. | ||
""" | ||
buffer = buffer.strip() | ||
try: | ||
obj, index = json_decoder.raw_decode(buffer) | ||
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():] | ||
return obj, rest | ||
except ValueError: | ||
return None | ||
|
||
|
||
def json_stream(stream): | ||
"""Given a stream of text, return a stream of json objects. | ||
This handles streams which are inconsistently buffered (some entries may | ||
be newline delimited, and others are not). | ||
""" | ||
return split_buffer(stream, json_splitter, json_decoder.decode) | ||
|
||
|
||
def line_splitter(buffer, separator=u'\n'): | ||
index = buffer.find(six.text_type(separator)) | ||
if index == -1: | ||
return None | ||
return buffer[:index + 1], buffer[index + 1:] | ||
|
||
|
||
def split_buffer(stream, splitter=None, decoder=lambda a: a): | ||
"""Given a generator which yields strings and a splitter function, | ||
joins all input, splits on the separator and yields each chunk. | ||
Unlike string.split(), each chunk includes the trailing | ||
separator, except for the last one if none was found on the end | ||
of the input. | ||
""" | ||
splitter = splitter or line_splitter | ||
buffered = six.text_type('') | ||
|
||
for data in stream_as_text(stream): | ||
buffered += data | ||
while True: | ||
buffer_split = splitter(buffered) | ||
if buffer_split is None: | ||
break | ||
|
||
item, buffered = buffer_split | ||
yield item | ||
|
||
if buffered: | ||
try: | ||
yield decoder(buffered) | ||
except Exception as e: | ||
raise StreamParseError(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# encoding: utf-8 | ||
from __future__ import absolute_import | ||
from __future__ import unicode_literals | ||
|
||
from docker.utils.json_stream import json_splitter, stream_as_text, json_stream | ||
|
||
|
||
class TestJsonSplitter(object): | ||
|
||
def test_json_splitter_no_object(self): | ||
data = '{"foo": "bar' | ||
assert json_splitter(data) is None | ||
|
||
def test_json_splitter_with_object(self): | ||
data = '{"foo": "bar"}\n \n{"next": "obj"}' | ||
assert json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') | ||
|
||
def test_json_splitter_leading_whitespace(self): | ||
data = '\n \r{"foo": "bar"}\n\n {"next": "obj"}' | ||
assert json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') | ||
|
||
|
||
class TestStreamAsText(object): | ||
|
||
def test_stream_with_non_utf_unicode_character(self): | ||
stream = [b'\xed\xf3\xf3'] | ||
output, = stream_as_text(stream) | ||
assert output == '���' | ||
|
||
def test_stream_with_utf_character(self): | ||
stream = ['ěĝ'.encode('utf-8')] | ||
output, = stream_as_text(stream) | ||
assert output == 'ěĝ' | ||
|
||
|
||
class TestJsonStream(object): | ||
|
||
def test_with_falsy_entries(self): | ||
stream = [ | ||
'{"one": "two"}\n{}\n', | ||
"[1, 2, 3]\n[]\n", | ||
] | ||
output = list(json_stream(stream)) | ||
assert output == [ | ||
{'one': 'two'}, | ||
{}, | ||
[1, 2, 3], | ||
[], | ||
] | ||
|
||
def test_with_leading_whitespace(self): | ||
stream = [ | ||
'\n \r\n {"one": "two"}{"x": 1}', | ||
' {"three": "four"}\t\t{"x": 2}' | ||
] | ||
output = list(json_stream(stream)) | ||
assert output == [ | ||
{'one': 'two'}, | ||
{'x': 1}, | ||
{'three': 'four'}, | ||
{'x': 2} | ||
] |