Skip to content

Commit 06df42d

Browse files
authored
Merge pull request #131 from lcmcninch/get-pages
Implement a means of getting more than 1000 data points
2 parents 92093d4 + b533482 commit 06df42d

File tree

3 files changed

+71
-10
lines changed

3 files changed

+71
-10
lines changed

Diff for: Adafruit_IO/client.py

+56-8
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@
2323
import json
2424
import platform
2525
import pkg_resources
26+
import re
27+
from urllib.parse import urlparse
28+
from urllib.parse import parse_qs
2629
# import logging
2730

2831
import requests
2932

3033
from .errors import RequestError, ThrottlingError
3134
from .model import Data, Feed, Group, Dashboard, Block, Layout
3235

36+
DEFAULT_PAGE_LIMIT = 100
37+
3338
# set outgoing version, pulled from setup.py
3439
version = pkg_resources.require("Adafruit_IO")[0].version
3540
default_headers = {
@@ -61,6 +66,9 @@ def __init__(self, username, key, proxies=None, base_url='https://io.adafruit.co
6166
# constructing the path.
6267
self.base_url = base_url.rstrip('/')
6368

69+
# Store the last response of a get or post
70+
self._last_response = None
71+
6472
@staticmethod
6573
def to_red(data):
6674
"""Hex color feed to red channel.
@@ -112,10 +120,12 @@ def _handle_error(response):
112120
def _compose_url(self, path):
113121
return '{0}/api/{1}/{2}/{3}'.format(self.base_url, 'v2', self.username, path)
114122

115-
def _get(self, path):
123+
def _get(self, path, params=None):
116124
response = requests.get(self._compose_url(path),
117125
headers=self._headers({'X-AIO-Key': self.key}),
118-
proxies=self.proxies)
126+
proxies=self.proxies,
127+
params=params)
128+
self._last_response = response
119129
self._handle_error(response)
120130
return response.json()
121131

@@ -125,6 +135,7 @@ def _post(self, path, data):
125135
'Content-Type': 'application/json'}),
126136
proxies=self.proxies,
127137
data=json.dumps(data))
138+
self._last_response = response
128139
self._handle_error(response)
129140
return response.json()
130141

@@ -133,6 +144,7 @@ def _delete(self, path):
133144
headers=self._headers({'X-AIO-Key': self.key,
134145
'Content-Type': 'application/json'}),
135146
proxies=self.proxies)
147+
self._last_response = response
136148
self._handle_error(response)
137149

138150
# Data functionality.
@@ -242,17 +254,53 @@ def receive_previous(self, feed):
242254
path = "feeds/{0}/data/previous".format(feed)
243255
return Data.from_dict(self._get(path))
244256

245-
def data(self, feed, data_id=None):
257+
def data(self, feed, data_id=None, max_results=DEFAULT_PAGE_LIMIT):
246258
"""Retrieve data from a feed. If data_id is not specified then all the data
247259
for the feed will be returned in an array.
248260
:param string feed: Name/Key/ID of Adafruit IO feed.
249261
:param string data_id: ID of the piece of data to delete.
262+
:param int max_results: The maximum number of results to return. To
263+
return all data, set to None.
250264
"""
251-
if data_id is None:
252-
path = "feeds/{0}/data".format(feed)
253-
return list(map(Data.from_dict, self._get(path)))
254-
path = "feeds/{0}/data/{1}".format(feed, data_id)
255-
return Data.from_dict(self._get(path))
265+
if max_results is None:
266+
res = self._get(f'feeds/{feed}/details')
267+
max_results = res['details']['data']['count']
268+
if data_id:
269+
path = "feeds/{0}/data/{1}".format(feed, data_id)
270+
return Data.from_dict(self._get(path))
271+
272+
params = {'limit': max_results} if max_results else None
273+
data = []
274+
path = "feeds/{0}/data".format(feed)
275+
while len(data) < max_results:
276+
data.extend(list(map(Data.from_dict, self._get(path,
277+
params=params))))
278+
nlink = self.get_next_link()
279+
if not nlink:
280+
break
281+
# Parse the link for the query parameters
282+
params = parse_qs(urlparse(nlink).query)
283+
if max_results:
284+
params['limit'] = max_results - len(data)
285+
return data
286+
287+
def get_next_link(self):
288+
"""Parse the `next` page URL in the pagination Link header.
289+
290+
This is necessary because of a bug in the API's implementation of the
291+
link header. If that bug is fixed, the link would be accesible by
292+
response.links['next']['url'] and this method would be broken.
293+
294+
:return: The url for the next page of data
295+
:rtype: str
296+
"""
297+
if not self._last_response:
298+
return
299+
link_header = self._last_response.headers['link']
300+
res = re.search('rel="next", <(.+?)>', link_header)
301+
if not res:
302+
return
303+
return res.groups()[0]
256304

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

Diff for: docs/data.rst

+13
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

Diff for: tests/test_client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def ensure_block_deleted(self, client, dashboard, block):
6464

6565
def empty_feed(self, client, feed):
6666
# Remove all the data from a specified feed (but don't delete the feed).
67-
data = client.data(feed)
67+
data = client.data(feed, max_results=None)
6868
for d in data:
6969
client.delete(feed, d.id)
7070

@@ -406,4 +406,4 @@ def test_layout_update_layout(self):
406406

407407

408408
if __name__ == "__main__":
409-
unittest.main()
409+
unittest.main()

0 commit comments

Comments
 (0)