Skip to content

Commit e9afd5b

Browse files
committed
Implement a "max_results" parameter for the data method, to allow for retrieval of more than 1000 data points.
This implementation is subject to an existing bug in the pagination link header in the API that will break when and if that bug is fixed.
1 parent e94e2d4 commit e9afd5b

File tree

3 files changed

+69
-10
lines changed

3 files changed

+69
-10
lines changed

Adafruit_IO/client.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@
2222
import json
2323
import platform
2424
import pkg_resources
25+
import re
26+
from urllib.parse import urlparse
27+
from urllib.parse import parse_qs
2528
# import logging
2629

2730
import requests
2831

2932
from .errors import RequestError, ThrottlingError
3033
from .model import Data, Feed, Group
3134

35+
API_PAGE_LIMIT = 1000
36+
3237
# set outgoing version, pulled from setup.py
3338
version = pkg_resources.require("Adafruit_IO")[0].version
3439
default_headers = {
@@ -60,6 +65,9 @@ def __init__(self, username, key, proxies=None, base_url='https://io.adafruit.co
6065
# constructing the path.
6166
self.base_url = base_url.rstrip('/')
6267

68+
# Store the last response of a get or post
69+
self._last_response = None
70+
6371
@staticmethod
6472
def to_red(data):
6573
"""Hex color feed to red channel.
@@ -116,6 +124,7 @@ def _get(self, path, params=None):
116124
headers=self._headers({'X-AIO-Key': self.key}),
117125
proxies=self.proxies,
118126
params=params)
127+
self._last_response = response
119128
self._handle_error(response)
120129
return response.json()
121130

@@ -125,6 +134,7 @@ def _post(self, path, data):
125134
'Content-Type': 'application/json'}),
126135
proxies=self.proxies,
127136
data=json.dumps(data))
137+
self._last_response = response
128138
self._handle_error(response)
129139
return response.json()
130140

@@ -133,6 +143,7 @@ def _delete(self, path):
133143
headers=self._headers({'X-AIO-Key': self.key,
134144
'Content-Type': 'application/json'}),
135145
proxies=self.proxies)
146+
self._last_response = response
136147
self._handle_error(response)
137148

138149
# Data functionality.
@@ -232,17 +243,52 @@ def receive_previous(self, feed):
232243
path = "feeds/{0}/data/previous".format(feed)
233244
return Data.from_dict(self._get(path))
234245

235-
def data(self, feed, data_id=None):
246+
def data(self, feed, data_id=None, max_results=API_PAGE_LIMIT):
236247
"""Retrieve data from a feed. If data_id is not specified then all the data
237248
for the feed will be returned in an array.
238249
:param string feed: Name/Key/ID of Adafruit IO feed.
239250
:param string data_id: ID of the piece of data to delete.
251+
:param int max_results: The maximum number of results to return. To
252+
return all data, set to None.
240253
"""
241-
if data_id is None:
242-
path = "feeds/{0}/data".format(feed)
243-
return list(map(Data.from_dict, self._get(path)))
244-
path = "feeds/{0}/data/{1}".format(feed, data_id)
245-
return Data.from_dict(self._get(path))
254+
if data_id:
255+
path = "feeds/{0}/data/{1}".format(feed, data_id)
256+
return Data.from_dict(self._get(path))
257+
258+
params = {'limit': max_results} if max_results else None
259+
data = []
260+
path = "feeds/{0}/data".format(feed)
261+
while True:
262+
data.extend(list(map(Data.from_dict, self._get(path,
263+
params=params))))
264+
nlink = self.get_next_link()
265+
if not nlink:
266+
break
267+
# Parse the link for the query parameters
268+
params = parse_qs(urlparse(nlink).query)
269+
if max_results:
270+
if len(data) >= max_results:
271+
break
272+
params['limit'] = max_results - len(data)
273+
return data
274+
275+
def get_next_link(self):
276+
"""Parse the `next` page URL in the pagination Link header.
277+
278+
This is necessary because of a bug in the API's implementation of the
279+
link header. If that bug is fixed, the link would be accesible by
280+
response.links['next']['url'] and this method would be broken.
281+
282+
:return: The url for the next page of data
283+
:rtype: str
284+
"""
285+
if not self._last_response:
286+
return
287+
link_header = self._last_response.headers['link']
288+
res = re.search('rel="next", <(.+?)>', link_header)
289+
if not res:
290+
return
291+
return res.groups()[0]
246292

247293
def create_data(self, feed, data):
248294
"""Create a new row of data in the specified feed.

docs/data.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ You can get all of the data for a feed by using the ``data(feed)`` method. The r
3333
for d in data:
3434
print('Data value: {0}'.format(d.value))
3535
36+
By default, the maximum number of data points returned is 1000. This limit can be changed by using the max_results parameter.
37+
38+
.. code-block:: python
39+
40+
# Get less than the default number of data points
41+
data = aio.data('Test', max_results=100)
42+
43+
# Get more than the default number of data points
44+
data = aio.data('Test', max_results=2000)
45+
46+
# Get all of the points
47+
data = aio.data('Test', max_results=None)
48+
3649
You can also get a specific value by ID by using the ``feeds(feed, data_id)`` method. This will return a single piece of feed data with the provided data ID if it exists in the feed. The returned object will be an instance of the Data class.
3750

3851

tests/test_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ class TestClient(base.IOTestCase):
2222

2323
# If your IP isn't put on the list of non-throttled IPs, uncomment the
2424
# function below to waste time between tests to prevent throttling.
25-
#def tearDown(self):
26-
time.sleep(30.0)
25+
def tearDown(self):
26+
time.sleep(30.0)
2727

2828
# Helper Methods
2929
def get_client(self):
@@ -48,7 +48,7 @@ def ensure_group_deleted(self, client, group):
4848

4949
def empty_feed(self, client, feed):
5050
# Remove all the data from a specified feed (but don't delete the feed).
51-
data = client.data(feed)
51+
data = client.data(feed, max_results=None)
5252
for d in data:
5353
client.delete(feed, d.id)
5454

@@ -138,7 +138,7 @@ def test_create_data(self):
138138
data = Data(value=42)
139139
result = aio.create_data('testfeed', data)
140140
self.assertEqual(int(result.value), 42)
141-
141+
142142
def test_location_data(self):
143143
"""receive_location
144144
"""

0 commit comments

Comments
 (0)