Skip to content

Commit c0ac736

Browse files
committed
Make ResponseReader more streamlike, so that it can be wrapped in a BuffererReader
1 parent 7a5f896 commit c0ac736

File tree

4 files changed

+93
-34
lines changed

4 files changed

+93
-34
lines changed

splunklib/binding.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import socket
3030
import ssl
3131
import urllib
32+
import io
3233

3334
from datetime import datetime
3435
from functools import wraps
@@ -1110,7 +1111,7 @@ def request(self, url, message, **kwargs):
11101111

11111112

11121113
# Converts an httplib response into a file-like object.
1113-
class ResponseReader(object):
1114+
class ResponseReader(io.RawIOBase):
11141115
"""This class provides a file-like interface for :class:`httplib` responses.
11151116
11161117
The ``ResponseReader`` class is intended to be a layer to unify the different
@@ -1164,6 +1165,23 @@ def read(self, size = None):
11641165
r = r + self._response.read(size)
11651166
return r
11661167

1168+
def readable(self):
1169+
""" Indicates that the response reader is readable."""
1170+
return True
1171+
1172+
def readinto(self, byte_array):
1173+
""" Read data into a byte array, upto the size of the byte array.
1174+
1175+
:param byte_array: A byte array/memory view to pour bytes into.
1176+
:type byte_array: ``bytearray`` or ``memoryview``
1177+
1178+
"""
1179+
max_size = len(byte_array)
1180+
data = self.read(max_size)
1181+
bytes_read = len(data)
1182+
byte_array[:bytes_read] = data
1183+
return bytes_read
1184+
11671185

11681186
def handler(key_file=None, cert_file=None, timeout=None):
11691187
"""This class returns an instance of the default HTTP request handler using

splunklib/results.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
"""The **splunklib.results** module provides a streaming XML reader for Splunk
15+
"""The **splunklib.results** module provides a streaming XML reader for Splunk
1616
search results.
1717
18-
Splunk search results can be returned in a variety of formats including XML,
19-
JSON, and CSV. To make it easier to stream search results in XML format, they
20-
are returned as a stream of XML *fragments*, not as a single XML document. This
21-
module supports incrementally reading one result record at a time from such a
18+
Splunk search results can be returned in a variety of formats including XML,
19+
JSON, and CSV. To make it easier to stream search results in XML format, they
20+
are returned as a stream of XML *fragments*, not as a single XML document. This
21+
module supports incrementally reading one result record at a time from such a
2222
result stream. This module also provides a friendly iterator-based interface for
23-
accessing search results while avoiding buffering the result set, which can be
23+
accessing search results while avoiding buffering the result set, which can be
2424
very large.
2525
2626
To use the reader, instantiate :class:`ResultsReader` on a search result stream
@@ -65,13 +65,13 @@ class Message(object):
6565
def __init__(self, type_, message):
6666
self.type = type_
6767
self.message = message
68-
68+
6969
def __repr__(self):
7070
return "%s: %s" % (self.type, self.message)
71-
71+
7272
def __eq__(self, other):
7373
return (self.type, self.message) == (other.type, other.message)
74-
74+
7575
def __hash__(self):
7676
return hash((self.type, self.message))
7777

@@ -149,18 +149,18 @@ def read(self, n=None):
149149
return response
150150

151151
class ResultsReader(object):
152-
"""This class returns dictionaries and Splunk messages from an XML results
152+
"""This class returns dictionaries and Splunk messages from an XML results
153153
stream.
154154
155-
``ResultsReader`` is iterable, and returns a ``dict`` for results, or a
156-
:class:`Message` object for Splunk messages. This class has one field,
157-
``is_preview``, which is ``True`` when the results are a preview from a
155+
``ResultsReader`` is iterable, and returns a ``dict`` for results, or a
156+
:class:`Message` object for Splunk messages. This class has one field,
157+
``is_preview``, which is ``True`` when the results are a preview from a
158158
running search, or ``False`` when the results are from a completed search.
159159
160-
This function has no network activity other than what is implicit in the
160+
This function has no network activity other than what is implicit in the
161161
stream it operates on.
162162
163-
:param `stream`: The stream to read from (any object that supports
163+
:param `stream`: The stream to read from (any object that supports
164164
``.read()``).
165165
166166
**Example**::
@@ -224,7 +224,7 @@ def _parse_results(self, stream):
224224
yield result
225225
result = None
226226
elem.clear()
227-
227+
228228
elif elem.tag == 'field' and result is not None:
229229
# We need the 'result is not None' check because
230230
# 'field' is also the element name in the <meta>
@@ -244,11 +244,11 @@ def _parse_results(self, stream):
244244
# arbitrarily large memory intead of
245245
# streaming.
246246
elem.clear()
247-
247+
248248
elif elem.tag in ('text', 'v') and event == 'end':
249249
values.append(elem.text.encode('utf8'))
250250
elem.clear()
251-
251+
252252
elif elem.tag == 'msg':
253253
if event == 'start':
254254
msg_type = elem.attrib['type']

tests/test_binding.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ def test_empty(self):
6666
self.assertTrue(response.empty)
6767
self.assertEqual(response.peek(10), "")
6868
self.assertEqual(response.read(10), "")
69+
70+
arr = bytearray(10)
71+
self.assertEqual(response.readinto(arr), 0)
72+
self.assertEqual(arr, bytearray(10))
6973
self.assertTrue(response.empty)
7074

7175
def test_read_past_end(self):
@@ -87,6 +91,42 @@ def test_read_partial(self):
8791
self.assertTrue(response.empty)
8892
self.assertEqual(response.read(), '')
8993

94+
def test_readable(self):
95+
txt = "abcd"
96+
response = binding.ResponseReader(StringIO(txt))
97+
self.assertTrue(response.readable())
98+
99+
def test_readinto_bytearray(self):
100+
txt = "Checking readinto works as expected"
101+
response = binding.ResponseReader(StringIO(txt))
102+
arr = bytearray(10)
103+
self.assertEqual(response.readinto(arr), 10)
104+
self.assertEqual(arr[:10], "Checking r")
105+
self.assertEqual(response.readinto(arr), 10)
106+
self.assertEqual(arr[:10], "eadinto wo")
107+
self.assertEqual(response.readinto(arr), 10)
108+
self.assertEqual(arr[:10], "rks as exp")
109+
self.assertEqual(response.readinto(arr), 5)
110+
self.assertEqual(arr[:5], "ected")
111+
self.assertTrue(response.empty)
112+
113+
def test_readinto_memoryview(self):
114+
txt = "Checking readinto works as expected"
115+
response = binding.ResponseReader(StringIO(txt))
116+
arr = bytearray(10)
117+
mv = memoryview(arr)
118+
self.assertEqual(response.readinto(mv), 10)
119+
self.assertEqual(arr[:10], "Checking r")
120+
self.assertEqual(response.readinto(mv), 10)
121+
self.assertEqual(arr[:10], "eadinto wo")
122+
self.assertEqual(response.readinto(mv), 10)
123+
self.assertEqual(arr[:10], "rks as exp")
124+
self.assertEqual(response.readinto(mv), 5)
125+
self.assertEqual(arr[:5], "ected")
126+
self.assertTrue(response.empty)
127+
128+
129+
90130
class TestUrlEncoded(BindingTestCase):
91131
def test_idempotent(self):
92132
a = UrlEncoded('abc')
@@ -95,7 +135,7 @@ def test_idempotent(self):
95135
def test_append(self):
96136
self.assertEqual(UrlEncoded('a') + UrlEncoded('b'),
97137
UrlEncoded('ab'))
98-
138+
99139
def test_append_string(self):
100140
self.assertEqual(UrlEncoded('a') + '%',
101141
UrlEncoded('a%'))
@@ -111,15 +151,15 @@ def test_chars(self):
111151
for char, code in [(' ', '%20'),
112152
('"', '%22'),
113153
('%', '%25')]:
114-
self.assertEqual(UrlEncoded(char),
154+
self.assertEqual(UrlEncoded(char),
115155
UrlEncoded(code, skip_encode=True))
116156

117157
def test_repr(self):
118158
self.assertEqual(repr(UrlEncoded('% %')), "UrlEncoded('% %')")
119159

120160
class TestAuthority(unittest.TestCase):
121161
def test_authority_default(self):
122-
self.assertEqual(binding._authority(),
162+
self.assertEqual(binding._authority(),
123163
"https://localhost:8089")
124164

125165
def test_ipv4_host(self):
@@ -137,7 +177,7 @@ def test_ipv6_host(self):
137177
def test_all_fields(self):
138178
self.assertEqual(
139179
binding._authority(
140-
scheme="http",
180+
scheme="http",
141181
host="splunk.utopia.net",
142182
port="471"),
143183
"http://splunk.utopia.net:471")
@@ -172,7 +212,7 @@ def test_user_without_role_fails(self):
172212

173213
def test_create_user(self):
174214
response = self.context.post(
175-
PATH_USERS, name=self.username,
215+
PATH_USERS, name=self.username,
176216
password=self.password, roles=self.roles)
177217
self.assertEqual(response.status, 201)
178218

@@ -265,7 +305,7 @@ def test_without_autologin(self):
265305
self.context.autologin = False
266306
self.assertEqual(self.context.get("/services").status, 200)
267307
self.context.logout()
268-
self.assertRaises(AuthenticationError,
308+
self.assertRaises(AuthenticationError,
269309
self.context.get, "/services")
270310

271311
class TestAbspath(BindingTestCase):
@@ -403,7 +443,7 @@ def isatom(body):
403443
class TestPluggableHTTP(testlib.SDKTestCase):
404444
# Verify pluggable HTTP reqeust handlers.
405445
def test_handlers(self):
406-
paths = ["/services", "authentication/users",
446+
paths = ["/services", "authentication/users",
407447
"search/jobs"]
408448
handlers = [binding.handler(), # default handler
409449
urllib2_handler]
@@ -421,7 +461,7 @@ def test_logout(self):
421461
response = self.context.get("/services")
422462
self.assertEqual(response.status, 200)
423463
self.context.logout()
424-
self.assertRaises(AuthenticationError,
464+
self.assertRaises(AuthenticationError,
425465
self.context.get, "/services")
426466
self.assertRaises(AuthenticationError,
427467
self.context.post, "/services")
@@ -512,11 +552,11 @@ def test_preexisting_token(self):
512552
opts["token"] = token
513553
opts["username"] = "boris the mad baboon"
514554
opts["password"] = "nothing real"
515-
555+
516556
newContext = binding.Context(**opts)
517557
response = newContext.get("/services")
518558
self.assertEqual(response.status, 200)
519-
559+
520560
socket = newContext.connect()
521561
socket.write("POST %s HTTP/1.1\r\n" % \
522562
self.context._abspath("some/path/to/post/to"))

tests/test_results.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
import testlib
1919
from time import sleep
2020
import splunklib.results as results
21+
import io
2122

2223

2324
class ResultsTestCase(testlib.SDKTestCase):
2425
def test_read_from_empty_result_set(self):
2526
job = self.service.jobs.create("search index=_internal_does_not_exist | head 2")
2627
while not job.is_done():
2728
sleep(0.5)
28-
self.assertEquals(0, len(list(results.ResultsReader(job.results()))))
29+
self.assertEquals(0, len(list(results.ResultsReader(io.BufferedReader(job.results())))))
2930

3031
def test_read_normal_results(self):
3132
xml_text = """
@@ -107,9 +108,9 @@ def test_read_normal_results(self):
107108
'sum(kb)': '5838.935649',
108109
},
109110
]
110-
111+
111112
self.assert_parsed_results_equals(xml_text, expected_results)
112-
113+
113114
def test_read_raw_field(self):
114115
xml_text = """
115116
<?xml version='1.0' encoding='UTF-8'?>
@@ -129,9 +130,9 @@ def test_read_raw_field(self):
129130
'_raw': '07-13-2012 09:27:27.307 -0700 INFO Metrics - group=search_concurrency, system total, active_hist_searches=0, active_realtime_searches=0',
130131
},
131132
]
132-
133+
133134
self.assert_parsed_results_equals(xml_text, expected_results)
134-
135+
135136
def assert_parsed_results_equals(self, xml_text, expected_results):
136137
results_reader = results.ResultsReader(StringIO(xml_text))
137138
actual_results = [x for x in results_reader]

0 commit comments

Comments
 (0)