1
+ import collections
1
2
import json
2
3
3
4
from urllib3 import connection_from_url
@@ -8,12 +9,14 @@ class AdafruitIOError(Exception):
8
9
"""Base class for all Adafruit IO request failures."""
9
10
pass
10
11
12
+
11
13
class RequestError (Exception ):
12
14
"""General error for a failed Adafruit IO request."""
13
15
def __init__ (self , response ):
14
16
super (RequestError , self ).__init__ ("Adafruit IO request failed: {0} {1}" .format (
15
17
response .status , response .reason ))
16
18
19
+
17
20
class ThrottlingError (AdafruitIOError ):
18
21
"""Too many requests have been made to Adafruit IO in a short period of time.
19
22
Reduce the rate of requests and try again later.
@@ -23,11 +26,46 @@ def __init__(self):
23
26
"requests in a short period of time. Please reduce the rate of requests " \
24
27
"and try again later." )
25
28
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
+
26
58
#fork of ApiClient Class: https://github.com/shazow/apiclient
27
59
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
+ """
28
63
BASE_URL = 'https://io.adafruit.com/'
29
64
30
65
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
+ """
31
69
self .key = key
32
70
self .rate_limit_lock = rate_limit_lock
33
71
self .connection_pool = self ._make_connection_pool (self .BASE_URL )
@@ -50,11 +88,13 @@ def _handle_error(sefl, response):
50
88
raise RequestError (response )
51
89
# Else do nothing if there was no error.
52
90
53
- def _handle_response (self , response ):
91
+ def _handle_response (self , response , expect_result ):
54
92
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.
56
96
57
- def _request (self , method , path , params = None ):
97
+ def _request (self , method , path , params = None , expect_result = True ):
58
98
if (method .lower () == "get" ):
59
99
url = self ._compose_get_url (path , params )
60
100
else :
@@ -65,47 +105,86 @@ def _request(self, method, path, params=None):
65
105
if (method .upper () == "GET" ):
66
106
r = self .connection_pool .urlopen (method .upper (), url , headers = headers )
67
107
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 ))
69
110
70
- return self ._handle_response (r )
111
+ return self ._handle_response (r , expect_result )
71
112
72
113
def _get (self , path , ** params ):
73
114
return self ._request ('GET' , path , params = params )
74
115
75
116
def _post (self , path , params ):
76
117
return self ._request ('POST' , path , params = params )
77
118
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
+ """
80
137
feed_name = quote (feed_name )
81
138
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 ))
102
176
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 ()))
109
188
110
189
#group functionality
111
190
def send_group (self , group_name , data ):
0 commit comments