Skip to content

Commit 196f71d

Browse files
committed
Document, refactor slightly, and add tests to REST API client.
1 parent a1667e3 commit 196f71d

File tree

7 files changed

+474
-70
lines changed

7 files changed

+474
-70
lines changed

Adafruit_IO/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .client import Client, AdafruitIOError, RequestError, ThrottlingError
1+
from .client import Client, AdafruitIOError, RequestError, ThrottlingError, Data
22
from .mqtt_client import MQTTClient

Adafruit_IO/client.py

Lines changed: 112 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import collections
12
import json
23

34
from urllib3 import connection_from_url
@@ -8,12 +9,14 @@ class AdafruitIOError(Exception):
89
"""Base class for all Adafruit IO request failures."""
910
pass
1011

12+
1113
class RequestError(Exception):
1214
"""General error for a failed Adafruit IO request."""
1315
def __init__(self, response):
1416
super(RequestError, self).__init__("Adafruit IO request failed: {0} {1}".format(
1517
response.status, response.reason))
1618

19+
1720
class ThrottlingError(AdafruitIOError):
1821
"""Too many requests have been made to Adafruit IO in a short period of time.
1922
Reduce the rate of requests and try again later.
@@ -23,11 +26,46 @@ def __init__(self):
2326
"requests in a short period of time. Please reduce the rate of requests " \
2427
"and try again later.")
2528

29+
30+
class Data(collections.namedtuple('Data', ['created_epoch', 'created_at',
31+
'updated_at', 'value', 'completed_at', 'feed_id', 'expiration', 'position',
32+
'id'])):
33+
"""Row of data from a feed. This is a simple class that just represents data
34+
returned from the Adafruit IO service. The value property has the value of the
35+
data row, and other properties like created_at or id represent the metadata
36+
of the data row.
37+
"""
38+
39+
@classmethod
40+
def from_response(cls, response):
41+
"""Create a new Data instance based on the response dict from an Adafruit IO
42+
request.
43+
"""
44+
# Be careful to support forward compatibility by only looking at attributes
45+
# which this Data class knows about (i.e. ignore anything else that's
46+
# unknown).
47+
# In this case iterate through all the fields of the named tuple and grab
48+
# their value from the response dict. If any field doesn't exist in the
49+
# response dict then set it to None.
50+
return cls(*[response.get(x, None) for x in cls._fields])
51+
52+
# Magic incantation to make all parameters to the constructor optional with a
53+
# default value of None. This is useful when creating an explicit Data instance
54+
# to pass to create_data with only the value or other properties set.
55+
Data.__new__.__defaults__ = tuple(None for x in Data._fields)
56+
57+
2658
#fork of ApiClient Class: https://github.com/shazow/apiclient
2759
class Client(object):
60+
"""Client instance for interacting with the Adafruit IO service using its REST
61+
API. Use this client class to send, receive, and enumerate feed data.
62+
"""
2863
BASE_URL = 'https://io.adafruit.com/'
2964

3065
def __init__(self, key, rate_limit_lock=None):
66+
"""Create an instance of the Adafruit IO REST API client. Key must be
67+
provided and set to your Adafruit IO access key value.
68+
"""
3169
self.key = key
3270
self.rate_limit_lock = rate_limit_lock
3371
self.connection_pool = self._make_connection_pool(self.BASE_URL)
@@ -50,11 +88,13 @@ def _handle_error(sefl, response):
5088
raise RequestError(response)
5189
# Else do nothing if there was no error.
5290

53-
def _handle_response(self, response):
91+
def _handle_response(self, response, expect_result):
5492
self._handle_error(response)
55-
return json.loads(response.data)
93+
if expect_result:
94+
return json.loads(response.data)
95+
# Else no result expected so just return.
5696

57-
def _request(self, method, path, params=None):
97+
def _request(self, method, path, params=None, expect_result=True):
5898
if (method.lower() == "get"):
5999
url = self._compose_get_url(path, params)
60100
else:
@@ -65,47 +105,86 @@ def _request(self, method, path, params=None):
65105
if (method.upper() == "GET"):
66106
r = self.connection_pool.urlopen(method.upper(), url, headers=headers)
67107
else:
68-
r = self.connection_pool.urlopen(method.upper(), url, headers=headers, body=json.dumps(params))
108+
r = self.connection_pool.urlopen(method.upper(), url, headers=headers,
109+
body=json.dumps(params))
69110

70-
return self._handle_response(r)
111+
return self._handle_response(r, expect_result)
71112

72113
def _get(self, path, **params):
73114
return self._request('GET', path, params=params)
74115

75116
def _post(self, path, params):
76117
return self._request('POST', path, params=params)
77118

78-
#stream functionality
79-
def send(self, feed_name, data):
119+
def _delete(self, path):
120+
return self._request('DELETE', path, expect_result=False)
121+
122+
#feed functionality
123+
def delete_feed(self, feed):
124+
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed name.
125+
"""
126+
feed = quote(feed)
127+
path = "api/feeds/{}".format(feed)
128+
self._delete(path)
129+
130+
#feed data functionality
131+
def send(self, feed_name, value):
132+
"""Helper function to simplify adding a value to a feed. Will find the
133+
specified feed by name or create a new feed if it doesn't exist, then will
134+
append the provided value to the feed. Returns a Data instance with details
135+
about the newly appended row of data.
136+
"""
80137
feed_name = quote(feed_name)
81138
path = "api/feeds/{}/data/send".format(feed_name)
82-
return self._post(path, {'value': data})
83-
84-
def receive(self, feed_name):
85-
feed_name = quote(feed_name)
86-
path = "api/feeds/{}/data/last".format(feed_name)
87-
return self._get(path)
88-
89-
def receive_next(self, feed_name):
90-
feed_name = quote(feed_name)
91-
path = "api/feeds/{}/data/next".format(feed_name)
92-
return self._get(path)
93-
94-
def receive_previous(self, feed_name):
95-
feed_name = quote(feed_name)
96-
path = "api/feeds/{}/data/last".format(feed_name)
97-
return self._get(path)
98-
99-
def streams(self, feed_id_or_key, stream_id=None):
100-
if stream_id is None:
101-
path = "api/feeds/{}/data".format(feed_id_or_key)
139+
return Data.from_response(self._post(path, {'value': value}))
140+
141+
def receive(self, feed):
142+
"""Retrieve the most recent value for the specified feed. Feed can be a
143+
feed ID, feed key, or feed name. Returns a Data instance whose value
144+
property holds the retrieved value.
145+
"""
146+
feed = quote(feed)
147+
path = "api/feeds/{}/data/last".format(feed)
148+
return Data.from_response(self._get(path))
149+
150+
def receive_next(self, feed):
151+
"""Retrieve the next unread value from the specified feed. Feed can be a
152+
feed ID, feed key, or feed name. Returns a Data instance whose value
153+
property holds the retrieved value.
154+
"""
155+
feed = quote(feed)
156+
path = "api/feeds/{}/data/next".format(feed)
157+
return Data.from_response(self._get(path))
158+
159+
def receive_previous(self, feed):
160+
"""Retrieve the previously read value from the specified feed. Feed can be
161+
a feed ID, feed key, or feed name. Returns a Data instance whose value
162+
property holds the retrieved value.
163+
"""
164+
feed = quote(feed)
165+
path = "api/feeds/{}/data/last".format(feed)
166+
return Data.from_response(self._get(path))
167+
168+
def data(self, feed, data_id=None):
169+
"""Retrieve data from a feed. Feed can be a feed ID, feed key, or feed name.
170+
Data_id is an optional id for a single data value to retrieve. If data_id
171+
is not specified then all the data for the feed will be returned in an array.
172+
"""
173+
if data_id is None:
174+
path = "api/feeds/{}/data".format(feed)
175+
return map(Data.from_response, self._get(path))
102176
else:
103-
path = "api/feeds/{}/data/{}".format(feed_id_or_key, stream_id)
104-
return self._get(path)
105-
106-
def create_stream(self, feed_id_or_key, data):
107-
path = "api/feeds/{}/data".format(feed_id_or_key)
108-
return self._post(path, data)
177+
path = "api/feeds/{}/data/{}".format(feed, data_id)
178+
return Data.from_response(self._get(path))
179+
180+
def create_data(self, feed, data):
181+
"""Create a new row of data in the specified feed. Feed can be a feed ID,
182+
feed key, or feed name. Data must be an instance of the Data class with at
183+
least a value property set on it. Returns a Data instance with details
184+
about the newly appended row of data.
185+
"""
186+
path = "api/feeds/{}/data".format(feed)
187+
return Data.from_response(self._post(path, data._asdict()))
109188

110189
#group functionality
111190
def send_group(self, group_name, data):

0 commit comments

Comments
 (0)