Major update:
- Refactored REST client to use requests library for Python 3 compatibility. - Added docstrings to all public methods & classes. - Fleshed out CRUD APIs for data, feed, group. - Wrote full suite of integration tests to verify changes. - Updated setup.py to bump version to 0.9 & depend on requests and paho-mqtt modules. - Updated and tested code to work with both Python 2 and 3. - Broke out errors into separate file, and added explicit data model classes. - General cleanup and preparation for public release.
This commit is contained in:
parent
2ee81791a2
commit
9900d508f5
19 changed files with 1354 additions and 361 deletions
|
|
@ -1,2 +1,24 @@
|
||||||
from .client import Client, AdafruitIOError, RequestError, ThrottlingError, Data
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
from .mqtt_client import MQTTClient
|
# Author: Tony DiCola
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
from .client import Client
|
||||||
|
from .mqtt_client import MQTTClient
|
||||||
|
from .errors import AdafruitIOError, RequestError, ThrottlingError
|
||||||
|
from .model import Data, Stream, Feed, Group
|
||||||
|
|
|
||||||
|
|
@ -1,216 +1,242 @@
|
||||||
import collections
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
|
# Authors: Justin Cooper & Tony DiCola
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from urllib3 import connection_from_url
|
import requests
|
||||||
from urllib import urlencode, quote
|
|
||||||
|
from .errors import RequestError, ThrottlingError
|
||||||
|
from .model import Data, Feed, Group
|
||||||
|
|
||||||
|
|
||||||
class AdafruitIOError(Exception):
|
|
||||||
"""Base class for all Adafruit IO request failures."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RequestError(Exception):
|
|
||||||
"""General error for a failed Adafruit IO request."""
|
|
||||||
def __init__(self, response):
|
|
||||||
super(RequestError, self).__init__("Adafruit IO request failed: {0} {1}".format(
|
|
||||||
response.status, response.reason))
|
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingError(AdafruitIOError):
|
|
||||||
"""Too many requests have been made to Adafruit IO in a short period of time.
|
|
||||||
Reduce the rate of requests and try again later.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
super(ThrottlingError, self).__init__("Exceeded the limit of Adafruit IO " \
|
|
||||||
"requests in a short period of time. Please reduce the rate of requests " \
|
|
||||||
"and try again later.")
|
|
||||||
|
|
||||||
|
|
||||||
class Data(collections.namedtuple('Data', ['created_epoch', 'created_at',
|
|
||||||
'updated_at', 'value', 'completed_at', 'feed_id', 'expiration', 'position',
|
|
||||||
'id'])):
|
|
||||||
"""Row of data from a feed. This is a simple class that just represents data
|
|
||||||
returned from the Adafruit IO service. The value property has the value of the
|
|
||||||
data row, and other properties like created_at or id represent the metadata
|
|
||||||
of the data row.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_response(cls, response):
|
|
||||||
"""Create a new Data instance based on the response dict from an Adafruit IO
|
|
||||||
request.
|
|
||||||
"""
|
|
||||||
# Be careful to support forward compatibility by only looking at attributes
|
|
||||||
# which this Data class knows about (i.e. ignore anything else that's
|
|
||||||
# unknown).
|
|
||||||
# In this case iterate through all the fields of the named tuple and grab
|
|
||||||
# their value from the response dict. If any field doesn't exist in the
|
|
||||||
# response dict then set it to None.
|
|
||||||
return cls(*[response.get(x, None) for x in cls._fields])
|
|
||||||
|
|
||||||
# Magic incantation to make all parameters to the constructor optional with a
|
|
||||||
# default value of None. This is useful when creating an explicit Data instance
|
|
||||||
# to pass to create_data with only the value or other properties set.
|
|
||||||
Data.__new__.__defaults__ = tuple(None for x in Data._fields)
|
|
||||||
|
|
||||||
|
|
||||||
#fork of ApiClient Class: https://github.com/shazow/apiclient
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
"""Client instance for interacting with the Adafruit IO service using its REST
|
"""Client instance for interacting with the Adafruit IO service using its
|
||||||
API. Use this client class to send, receive, and enumerate feed data.
|
REST API. Use this client class to send, receive, and enumerate feed data.
|
||||||
"""
|
|
||||||
BASE_URL = 'https://io.adafruit.com/'
|
|
||||||
|
|
||||||
def __init__(self, key, rate_limit_lock=None):
|
|
||||||
"""Create an instance of the Adafruit IO REST API client. Key must be
|
|
||||||
provided and set to your Adafruit IO access key value.
|
|
||||||
"""
|
"""
|
||||||
self.key = key
|
|
||||||
self.rate_limit_lock = rate_limit_lock
|
|
||||||
self.connection_pool = self._make_connection_pool(self.BASE_URL)
|
|
||||||
|
|
||||||
def _make_connection_pool(self, url):
|
def __init__(self, key, proxies=None, base_url='https://io.adafruit.com'):
|
||||||
return connection_from_url(url)
|
"""Create an instance of the Adafruit IO REST API client. Key must be
|
||||||
|
provided and set to your Adafruit IO access key value. Optionaly
|
||||||
|
provide a proxies dict in the format used by the requests library, and
|
||||||
|
base_url to point at a different Adafruit IO service (the default is
|
||||||
|
the production Adafruit IO service over SSL).
|
||||||
|
"""
|
||||||
|
self.key = key
|
||||||
|
self.proxies = proxies
|
||||||
|
# Save URL without trailing slash as it will be added later when
|
||||||
|
# constructing the path.
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
|
||||||
def _compose_url(self, path):
|
def _compose_url(self, path):
|
||||||
return self.BASE_URL + path
|
return '{0}/{1}'.format(self.base_url, path)
|
||||||
|
|
||||||
def _compose_get_url(self, path, params=None):
|
def _handle_error(sefl, response):
|
||||||
return self.BASE_URL + path + '?' + urlencode(params)
|
# Handle explicit errors.
|
||||||
|
if response.status_code == 429:
|
||||||
|
raise ThrottlingError()
|
||||||
|
# Handle all other errors (400 & 500 level HTTP responses)
|
||||||
|
elif response.status_code >= 400:
|
||||||
|
raise RequestError(response)
|
||||||
|
# Else do nothing if there was no error.
|
||||||
|
|
||||||
def _handle_error(sefl, response):
|
def _get(self, path):
|
||||||
# Handle explicit errors.
|
response = requests.get(self._compose_url(path),
|
||||||
if response.status == 429:
|
headers={'X-AIO-Key': self.key},
|
||||||
raise ThrottlingError()
|
proxies=self.proxies)
|
||||||
# Handle all other errors (400 & 500 level HTTP responses)
|
self._handle_error(response)
|
||||||
elif response.status >= 400:
|
return response.json()
|
||||||
raise RequestError(response)
|
|
||||||
# Else do nothing if there was no error.
|
|
||||||
|
|
||||||
def _handle_response(self, response, expect_result):
|
def _post(self, path, data):
|
||||||
self._handle_error(response)
|
response = requests.post(self._compose_url(path),
|
||||||
if expect_result:
|
headers={'X-AIO-Key': self.key,
|
||||||
return json.loads(response.data)
|
'Content-Type': 'application/json'},
|
||||||
# Else no result expected so just return.
|
proxies=self.proxies,
|
||||||
|
data=json.dumps(data))
|
||||||
|
self._handle_error(response)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
def _request(self, method, path, params=None, expect_result=True):
|
def _delete(self, path):
|
||||||
if (method.lower() == "get"):
|
response = requests.delete(self._compose_url(path),
|
||||||
url = self._compose_get_url(path, params)
|
headers={'X-AIO-Key': self.key,
|
||||||
else:
|
'Content-Type': 'application/json'},
|
||||||
url = self._compose_url(path)
|
proxies=self.proxies)
|
||||||
|
self._handle_error(response)
|
||||||
|
|
||||||
self.rate_limit_lock and self.rate_limit_lock.acquire()
|
# Data functionality.
|
||||||
headers = {"X-AIO-Key": self.key, 'Content-Type':'application/json'}
|
def send(self, feed_name, value):
|
||||||
if (method.upper() == "GET"):
|
"""Helper function to simplify adding a value to a feed. Will find the
|
||||||
r = self.connection_pool.urlopen(method.upper(), url, headers=headers)
|
specified feed by name or create a new feed if it doesn't exist, then
|
||||||
else:
|
will append the provided value to the feed. Returns a Data instance
|
||||||
r = self.connection_pool.urlopen(method.upper(), url, headers=headers,
|
with details about the newly appended row of data.
|
||||||
body=json.dumps(params))
|
"""
|
||||||
|
path = "api/feeds/{0}/data/send".format(feed_name)
|
||||||
|
return Data.from_dict(self._post(path, {'value': value}))
|
||||||
|
|
||||||
return self._handle_response(r, expect_result)
|
def receive(self, feed):
|
||||||
|
"""Retrieve the most recent value for the specified feed. Feed can be a
|
||||||
|
feed ID, feed key, or feed name. Returns a Data instance whose value
|
||||||
|
property holds the retrieved value.
|
||||||
|
"""
|
||||||
|
path = "api/feeds/{0}/data/last".format(feed)
|
||||||
|
return Data.from_dict(self._get(path))
|
||||||
|
|
||||||
def _get(self, path, **params):
|
def receive_next(self, feed):
|
||||||
return self._request('GET', path, params=params)
|
"""Retrieve the next unread value from the specified feed. Feed can be
|
||||||
|
a feed ID, feed key, or feed name. Returns a Data instance whose value
|
||||||
|
property holds the retrieved value.
|
||||||
|
"""
|
||||||
|
path = "api/feeds/{0}/data/next".format(feed)
|
||||||
|
return Data.from_dict(self._get(path))
|
||||||
|
|
||||||
def _post(self, path, params):
|
def receive_previous(self, feed):
|
||||||
return self._request('POST', path, params=params)
|
"""Retrieve the previous unread value from the specified feed. Feed can
|
||||||
|
be a feed ID, feed key, or feed name. Returns a Data instance whose
|
||||||
|
value property holds the retrieved value.
|
||||||
|
"""
|
||||||
|
path = "api/feeds/{0}/data/previous".format(feed)
|
||||||
|
return Data.from_dict(self._get(path))
|
||||||
|
|
||||||
def _delete(self, path):
|
def data(self, feed, data_id=None):
|
||||||
return self._request('DELETE', path, expect_result=False)
|
"""Retrieve data from a feed. Feed can be a feed ID, feed key, or feed
|
||||||
|
name. Data_id is an optional id for a single data value to retrieve.
|
||||||
|
If data_id is not specified then all the data for the feed will be
|
||||||
|
returned in an array.
|
||||||
|
"""
|
||||||
|
if data_id is None:
|
||||||
|
path = "api/feeds/{0}/data".format(feed)
|
||||||
|
return list(map(Data.from_dict, self._get(path)))
|
||||||
|
else:
|
||||||
|
path = "api/feeds/{0}/data/{1}".format(feed, data_id)
|
||||||
|
return Data.from_dict(self._get(path))
|
||||||
|
|
||||||
#feed functionality
|
def create_data(self, feed, data):
|
||||||
def delete_feed(self, feed):
|
"""Create a new row of data in the specified feed. Feed can be a feed
|
||||||
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed name.
|
ID, feed key, or feed name. Data must be an instance of the Data class
|
||||||
"""
|
with at least a value property set on it. Returns a Data instance with
|
||||||
feed = quote(feed)
|
details about the newly appended row of data.
|
||||||
path = "api/feeds/{}".format(feed)
|
"""
|
||||||
self._delete(path)
|
path = "api/feeds/{0}/data".format(feed)
|
||||||
|
return Data.from_dict(self._post(path, data._asdict()))
|
||||||
|
|
||||||
#feed data functionality
|
def delete(self, feed, data_id):
|
||||||
def send(self, feed_name, value):
|
"""Delete data from a feed. Feed can be a feed ID, feed key, or feed
|
||||||
"""Helper function to simplify adding a value to a feed. Will find the
|
name. Data_id must be the ID of the piece of data to delete.
|
||||||
specified feed by name or create a new feed if it doesn't exist, then will
|
"""
|
||||||
append the provided value to the feed. Returns a Data instance with details
|
path = "api/feeds/{0}/data/{1}".format(feed, data_id)
|
||||||
about the newly appended row of data.
|
self._delete(path)
|
||||||
"""
|
|
||||||
feed_name = quote(feed_name)
|
|
||||||
path = "api/feeds/{}/data/send".format(feed_name)
|
|
||||||
return Data.from_response(self._post(path, {'value': value}))
|
|
||||||
|
|
||||||
def receive(self, feed):
|
# Feed functionality.
|
||||||
"""Retrieve the most recent value for the specified feed. Feed can be a
|
def feeds(self, feed=None):
|
||||||
feed ID, feed key, or feed name. Returns a Data instance whose value
|
"""Retrieve a list of all feeds, or the specified feed. If feed is not
|
||||||
property holds the retrieved value.
|
specified a list of all feeds will be returned. If feed is specified it
|
||||||
"""
|
can be a feed name, key, or ID and the requested feed will be returned.
|
||||||
feed = quote(feed)
|
"""
|
||||||
path = "api/feeds/{}/data/last".format(feed)
|
if feed is None:
|
||||||
return Data.from_response(self._get(path))
|
path = "api/feeds"
|
||||||
|
return list(map(Feed.from_dict, self._get(path)))
|
||||||
|
else:
|
||||||
|
path = "api/feeds/{0}".format(feed)
|
||||||
|
return Feed.from_dict(self._get(path))
|
||||||
|
|
||||||
def receive_next(self, feed):
|
def create_feed(self, feed):
|
||||||
"""Retrieve the next unread value from the specified feed. Feed can be a
|
"""Create the specified feed. Feed should be an instance of the Feed
|
||||||
feed ID, feed key, or feed name. Returns a Data instance whose value
|
type with at least the name property set.
|
||||||
property holds the retrieved value.
|
"""
|
||||||
"""
|
path = "api/feeds/"
|
||||||
feed = quote(feed)
|
return Feed.from_dict(self._post(path, feed._asdict()))
|
||||||
path = "api/feeds/{}/data/next".format(feed)
|
|
||||||
return Data.from_response(self._get(path))
|
|
||||||
|
|
||||||
def receive_previous(self, feed):
|
def delete_feed(self, feed):
|
||||||
"""Retrieve the previously read value from the specified feed. Feed can be
|
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed
|
||||||
a feed ID, feed key, or feed name. Returns a Data instance whose value
|
name.
|
||||||
property holds the retrieved value.
|
"""
|
||||||
"""
|
path = "api/feeds/{0}".format(feed)
|
||||||
feed = quote(feed)
|
self._delete(path)
|
||||||
path = "api/feeds/{}/data/last".format(feed)
|
|
||||||
return Data.from_response(self._get(path))
|
|
||||||
|
|
||||||
def data(self, feed, data_id=None):
|
# Group functionality.
|
||||||
"""Retrieve data from a feed. Feed can be a feed ID, feed key, or feed name.
|
def send_group(self, group_name, data):
|
||||||
Data_id is an optional id for a single data value to retrieve. If data_id
|
"""Update all feeds in a group with one call. Group_name should be the
|
||||||
is not specified then all the data for the feed will be returned in an array.
|
name of a group to update. Data should be a dict with an item for each
|
||||||
"""
|
feed in the group, where the key is the feed name and value is the new
|
||||||
if data_id is None:
|
data row value. For example a group 'TestGroup' with feeds 'FeedOne'
|
||||||
path = "api/feeds/{}/data".format(feed)
|
and 'FeedTwo' could be updated by calling:
|
||||||
return map(Data.from_response, self._get(path))
|
|
||||||
else:
|
send_group('TestGroup', {'FeedOne': 'value1', 'FeedTwo': 10})
|
||||||
path = "api/feeds/{}/data/{}".format(feed, data_id)
|
|
||||||
return Data.from_response(self._get(path))
|
This would add the value 'value1' to the feed 'FeedOne' and add the
|
||||||
|
value 10 to the feed 'FeedTwo'.
|
||||||
|
|
||||||
def create_data(self, feed, data):
|
After a successful update an instance of Group will be returned with
|
||||||
"""Create a new row of data in the specified feed. Feed can be a feed ID,
|
metadata about the updated group.
|
||||||
feed key, or feed name. Data must be an instance of the Data class with at
|
"""
|
||||||
least a value property set on it. Returns a Data instance with details
|
path = "api/groups/{0}/send".format(group_name)
|
||||||
about the newly appended row of data.
|
return Group.from_dict(self._post(path, {'value': data}))
|
||||||
"""
|
|
||||||
path = "api/feeds/{}/data".format(feed)
|
|
||||||
return Data.from_response(self._post(path, data._asdict()))
|
|
||||||
|
|
||||||
#group functionality
|
def receive_group(self, group):
|
||||||
def send_group(self, group_name, data):
|
"""Retrieve the most recent value for the specified group. Group can be
|
||||||
group_name = quote(group_name)
|
a group ID, group key, or group name. Returns a Group instance whose
|
||||||
path = "api/groups/{}/send".format(group_name)
|
feeds property holds an array of Feed instances associated with the group.
|
||||||
return self._post(path, {'value': data})
|
"""
|
||||||
|
path = "api/groups/{0}/last".format(group)
|
||||||
|
return Group.from_dict(self._get(path))
|
||||||
|
|
||||||
def receive_group(self, group_name):
|
def receive_next_group(self, group):
|
||||||
group_name = quote(group_name)
|
"""Retrieve the next unread value from the specified group. Group can
|
||||||
path = "api/groups/{}/last".format(group_name)
|
be a group ID, group key, or group name. Returns a Group instance whose
|
||||||
return self._get(path)
|
feeds property holds an array of Feed instances associated with the
|
||||||
|
group.
|
||||||
|
"""
|
||||||
|
path = "api/groups/{0}/next".format(group)
|
||||||
|
return Group.from_dict(self._get(path))
|
||||||
|
|
||||||
def receive_next_group(self, group_name):
|
def receive_previous_group(self, group):
|
||||||
group_name = quote(group_name)
|
"""Retrieve the previous unread value from the specified group. Group
|
||||||
path = "api/groups/{}/next".format(group_name)
|
can be a group ID, group key, or group name. Returns a Group instance
|
||||||
return self._get(path)
|
whose feeds property holds an array of Feed instances associated with
|
||||||
|
the group.
|
||||||
|
"""
|
||||||
|
path = "api/groups/{0}/previous".format(group)
|
||||||
|
return Group.from_dict(self._get(path))
|
||||||
|
|
||||||
def receive_previous_group(self, group_name):
|
def groups(self, group=None):
|
||||||
group_name = quote(group_name)
|
"""Retrieve a list of all groups, or the specified group. If group is
|
||||||
path = "api/groups/{}/last".format(group_name)
|
not specified a list of all groups will be returned. If group is
|
||||||
return self._get(path)
|
specified it can be a group name, key, or ID and the requested group
|
||||||
|
will be returned.
|
||||||
|
"""
|
||||||
|
if group is None:
|
||||||
|
path = "api/groups/"
|
||||||
|
return list(map(Group.from_dict, self._get(path)))
|
||||||
|
else:
|
||||||
|
path = "api/groups/{0}".format(group)
|
||||||
|
return Group.from_dict(self._get(path))
|
||||||
|
|
||||||
def groups(self, group_id_or_key):
|
def create_group(self, group):
|
||||||
path = "api/groups/{}".format(group_id_or_key)
|
"""Create the specified group. Group should be an instance of the Group
|
||||||
return self._get(path)
|
type with at least the name and feeds property set.
|
||||||
|
"""
|
||||||
|
path = "api/groups/"
|
||||||
|
return Group.from_dict(self._post(path, group._asdict()))
|
||||||
|
|
||||||
def create_group(self, group_id_or_key, data):
|
def delete_group(self, group):
|
||||||
path = "api/groups/{}".format(group_id_or_key)
|
"""Delete the specified group. Group can be a group ID, group key, or
|
||||||
return self._post(path, data)
|
group name.
|
||||||
|
"""
|
||||||
|
path = "api/groups/{0}".format(group)
|
||||||
|
self._delete(path)
|
||||||
|
|
|
||||||
40
Adafruit_IO/errors.py
Normal file
40
Adafruit_IO/errors.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
|
# Author: Tony DiCola
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
class AdafruitIOError(Exception):
|
||||||
|
"""Base class for all Adafruit IO request failures."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RequestError(Exception):
|
||||||
|
"""General error for a failed Adafruit IO request."""
|
||||||
|
def __init__(self, response):
|
||||||
|
super(RequestError, self).__init__("Adafruit IO request failed: {0} {1}".format(
|
||||||
|
response.status_code, response.reason))
|
||||||
|
|
||||||
|
|
||||||
|
class ThrottlingError(AdafruitIOError):
|
||||||
|
"""Too many requests have been made to Adafruit IO in a short period of time.
|
||||||
|
Reduce the rate of requests and try again later.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(ThrottlingError, self).__init__("Exceeded the limit of Adafruit IO " \
|
||||||
|
"requests in a short period of time. Please reduce the rate of requests " \
|
||||||
|
"and try again later.")
|
||||||
117
Adafruit_IO/model.py
Normal file
117
Adafruit_IO/model.py
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
|
# Author: Tony DiCola
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
from collections import namedtuple
|
||||||
|
# Handle python 2 and 3 (where map functions like itertools.imap)
|
||||||
|
try:
|
||||||
|
from itertools import imap as map
|
||||||
|
except ImportError:
|
||||||
|
# Ignore import error on python 3 since map already behaves as expected.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# List of fields/properties that are present on a data object from IO.
|
||||||
|
DATA_FIELDS = [ 'created_epoch',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'value',
|
||||||
|
'completed_at',
|
||||||
|
'feed_id',
|
||||||
|
'expiration',
|
||||||
|
'position',
|
||||||
|
'id' ]
|
||||||
|
|
||||||
|
STREAM_FIELDS = [ 'completed_at',
|
||||||
|
'created_at',
|
||||||
|
'id',
|
||||||
|
'value' ]
|
||||||
|
|
||||||
|
FEED_FIELDS = [ 'last_value_at',
|
||||||
|
'name',
|
||||||
|
'stream',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'unit_type',
|
||||||
|
'mode',
|
||||||
|
'key',
|
||||||
|
'unit_symbol',
|
||||||
|
'fixed',
|
||||||
|
'last_value',
|
||||||
|
'id' ]
|
||||||
|
|
||||||
|
GROUP_FIELDS = [ 'description',
|
||||||
|
'source_keys',
|
||||||
|
'id',
|
||||||
|
'source',
|
||||||
|
'key',
|
||||||
|
'feeds',
|
||||||
|
'properties',
|
||||||
|
'name' ]
|
||||||
|
|
||||||
|
|
||||||
|
# These are very simple data model classes that are based on namedtuple. This is
|
||||||
|
# to keep the classes simple and prevent any confusion around updating data
|
||||||
|
# locally and forgetting to send those updates back up to the IO service (since
|
||||||
|
# tuples are immutable you can't change them!). Depending on how people use the
|
||||||
|
# client it might be prudent to revisit this decision and consider making these
|
||||||
|
# full fledged classes that are mutable.
|
||||||
|
Data = namedtuple('Data', DATA_FIELDS)
|
||||||
|
Stream = namedtuple('Stream', STREAM_FIELDS)
|
||||||
|
Feed = namedtuple('Feed', FEED_FIELDS)
|
||||||
|
Group = namedtuple('Group', GROUP_FIELDS)
|
||||||
|
|
||||||
|
|
||||||
|
# Magic incantation to make all parameters to the initializers optional with a
|
||||||
|
# default value of None.
|
||||||
|
Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS)
|
||||||
|
Stream.__new__.__defaults__ = tuple(None for x in STREAM_FIELDS)
|
||||||
|
Feed.__new__.__defaults__ = tuple(None for x in FEED_FIELDS)
|
||||||
|
Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS)
|
||||||
|
|
||||||
|
|
||||||
|
# Define methods to convert from dicts to the data types.
|
||||||
|
def _from_dict(cls, data):
|
||||||
|
# Convert dict to call to class initializer (to work with the data types
|
||||||
|
# base on namedtuple). However be very careful to preserve forwards
|
||||||
|
# compatibility by ignoring any attributes in the dict which are unknown
|
||||||
|
# by the data type.
|
||||||
|
params = {x: data.get(x, None) for x in cls._fields}
|
||||||
|
return cls(**params)
|
||||||
|
|
||||||
|
|
||||||
|
def _feed_from_dict(cls, data):
|
||||||
|
params = {x: data.get(x, None) for x in cls._fields}
|
||||||
|
# Parse the stream if provided and generate a stream instance.
|
||||||
|
params['stream'] = Stream.from_dict(data.get('stream', {}))
|
||||||
|
return cls(**params)
|
||||||
|
|
||||||
|
|
||||||
|
def _group_from_dict(cls, data):
|
||||||
|
params = {x: data.get(x, None) for x in cls._fields}
|
||||||
|
# Parse the feeds if they're provided and generate feed instances.
|
||||||
|
params['feeds'] = tuple(map(Feed.from_dict, data.get('feeds', [])))
|
||||||
|
return cls(**params)
|
||||||
|
|
||||||
|
|
||||||
|
# Now add the from_dict class methods defined above to the data types.
|
||||||
|
Data.from_dict = classmethod(_from_dict)
|
||||||
|
Stream.from_dict = classmethod(_from_dict)
|
||||||
|
Feed.from_dict = classmethod(_feed_from_dict)
|
||||||
|
Group.from_dict = classmethod(_group_from_dict)
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
# MQTT-based client for Adafruit.IO
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
# Author: Tony DiCola
|
||||||
#
|
|
||||||
# Supports publishing and subscribing to feed changes from Adafruit IO using
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
# the MQTT protcol.
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
#
|
# in the Software without restriction, including without limitation the rights
|
||||||
# Depends on the following Python libraries:
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
# - paho-mqtt: Paho MQTT client for python.
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
|
||||||
SERVICE_HOST = 'io.adafruit.com'
|
# How long to wait before sending a keep alive (paho-mqtt configuration).
|
||||||
SERVICE_PORT = 1883
|
|
||||||
KEEP_ALIVE_SEC = 3600 # One minute
|
KEEP_ALIVE_SEC = 3600 # One minute
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -23,12 +34,14 @@ class MQTTClient(object):
|
||||||
using the MQTT protocol.
|
using the MQTT protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, key):
|
def __init__(self, key, service_host='io.adafruit.com', service_port=1883):
|
||||||
"""Create instance of MQTT client.
|
"""Create instance of MQTT client.
|
||||||
|
|
||||||
Required parameters:
|
Required parameters:
|
||||||
- key: The Adafruit.IO access key for your account.
|
- key: The Adafruit.IO access key for your account.
|
||||||
"""
|
"""
|
||||||
|
self._service_host = service_host
|
||||||
|
self._service_port = service_port
|
||||||
# Initialize event callbacks to be None so they don't fire.
|
# Initialize event callbacks to be None so they don't fire.
|
||||||
self.on_connect = None
|
self.on_connect = None
|
||||||
self.on_disconnect = None
|
self.on_disconnect = None
|
||||||
|
|
@ -69,11 +82,12 @@ class MQTTClient(object):
|
||||||
def _mqtt_message(self, client, userdata, msg):
|
def _mqtt_message(self, client, userdata, msg):
|
||||||
logger.debug('Client on_message called.')
|
logger.debug('Client on_message called.')
|
||||||
# Parse out the feed id and call on_message callback.
|
# Parse out the feed id and call on_message callback.
|
||||||
# Assumes topic looks like "api/feeds/{feed_id}/data/receive.json"
|
# Assumes topic looks like "api/feeds/{feed}/data/receive.json"
|
||||||
if self.on_message is not None and msg.topic.startswith('api/feeds/') \
|
if self.on_message is not None and msg.topic.startswith('api/feeds/') \
|
||||||
and len(msg.topic) >= 28:
|
and len(msg.topic) >= 28:
|
||||||
feed_id = msg.topic[10:-18]
|
feed = msg.topic[10:-18]
|
||||||
self.on_message(self, feed_id, msg.payload)
|
payload = '' if msg.payload is None else msg.payload.decode('utf-8')
|
||||||
|
self.on_message(self, feed, payload)
|
||||||
|
|
||||||
def connect(self, **kwargs):
|
def connect(self, **kwargs):
|
||||||
"""Connect to the Adafruit.IO service. Must be called before any loop
|
"""Connect to the Adafruit.IO service. Must be called before any loop
|
||||||
|
|
@ -85,7 +99,7 @@ class MQTTClient(object):
|
||||||
if self._connected:
|
if self._connected:
|
||||||
return
|
return
|
||||||
# Connect to the Adafruit IO MQTT service.
|
# Connect to the Adafruit IO MQTT service.
|
||||||
self._client.connect(SERVICE_HOST, port=SERVICE_PORT,
|
self._client.connect(self._service_host, port=self._service_port,
|
||||||
keepalive=KEEP_ALIVE_SEC, **kwargs)
|
keepalive=KEEP_ALIVE_SEC, **kwargs)
|
||||||
|
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
|
|
@ -94,7 +108,7 @@ class MQTTClient(object):
|
||||||
return self._connected
|
return self._connected
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
# Disconnect MQTT client if connected.
|
"""Disconnect MQTT client if connected."""
|
||||||
if self._connected:
|
if self._connected:
|
||||||
self._client.disconnect()
|
self._client.disconnect()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,12 @@
|
||||||
|
0.9.0
|
||||||
|
----
|
||||||
|
Author: Tony DiCola
|
||||||
|
- Added REST API support for all feed, data, group create/get/update/delete APIs.
|
||||||
|
- Added explicit data model classes.
|
||||||
|
- Added many integration tests to verify client & service.
|
||||||
|
- Added docstrings to all public functions and classes.
|
||||||
|
- Ported to work with Python 2 and 3.
|
||||||
|
|
||||||
0.0.1
|
0.0.1
|
||||||
----
|
----
|
||||||
Initial Changelog
|
Initial Changelog
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Copyright (c) 2014 Adafruit
|
Copyright (c) 2014 Adafruit
|
||||||
Author: Justin Cooper
|
Author: Justin Cooper and Tony DiCola
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
|
|
|
||||||
287
README.md
287
README.md
|
|
@ -1,11 +1,14 @@
|
||||||
# adafruit-io
|
# adafruit-io
|
||||||
|
|
||||||
A [Python][1] client for use with with [io.adafruit.com][2].
|
A [Python][1] client for use with with [io.adafruit.com][2]. Compatible with
|
||||||
|
both Python 2 and Python 3.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Easy Installation
|
### Easy Installation
|
||||||
|
|
||||||
|
**NOTE: MODULE IS NOT YET ON PYPA SO THIS DOES NOT WORK YET. SKIP TO MANUAL INSTALL BELOW.**
|
||||||
|
|
||||||
If you have [pip installed](https://pip.pypa.io/en/latest/installing.html)
|
If you have [pip installed](https://pip.pypa.io/en/latest/installing.html)
|
||||||
(typically with ````apt-get install python-pip```` on a Debian/Ubuntu-based
|
(typically with ````apt-get install python-pip```` on a Debian/Ubuntu-based
|
||||||
system) then run:
|
system) then run:
|
||||||
|
|
@ -31,25 +34,24 @@ At a high level the Adafruit IO python client provides two interfaces to the
|
||||||
service:
|
service:
|
||||||
|
|
||||||
* A thin wrapper around the REST-based API. This is good for simple request and
|
* A thin wrapper around the REST-based API. This is good for simple request and
|
||||||
response applications.
|
response applications like logging data.
|
||||||
|
|
||||||
* An MQTT client (based on [paho-mqtt](https://pypi.python.org/pypi/paho-mqtt))
|
* A MQTT client (based on [paho-mqtt](https://pypi.python.org/pypi/paho-mqtt))
|
||||||
which can publish and subscribe to feeds so it is immediately alerted of changes.
|
which can publish and subscribe to feeds so it is immediately alerted of changes.
|
||||||
This is good for applications which need to know when something has changed as
|
This is good for applications which need to know when something has changed as
|
||||||
quickly as possible, but requires keeping a connection to the service open at
|
quickly as possible.
|
||||||
all times.
|
|
||||||
|
|
||||||
To use either interface you'll first need to import the python client by adding
|
To use either interface you'll first need to import the python client by adding
|
||||||
an import such as the following at the top of your program:
|
an import such as the following at the top of your program:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import Adafruit_IO
|
from Adafruit_IO import *
|
||||||
```
|
```
|
||||||
|
|
||||||
Then a REST API client can be created with code like:
|
Then a REST API client can be created with code like:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
aio = Adafruit_IO.Client('xxxxxxxxxxxx')
|
aio = Client('xxxxxxxxxxxx')
|
||||||
```
|
```
|
||||||
|
|
||||||
Where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
Where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
||||||
|
|
@ -57,7 +59,7 @@ Where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
||||||
Alternatively an MQTT client can be created with code like:
|
Alternatively an MQTT client can be created with code like:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
mqtt = Adafruit_IO.MQTTClient('xxxxxxxxxxxx')
|
mqtt = MQTTClient('xxxxxxxxxxxx')
|
||||||
```
|
```
|
||||||
|
|
||||||
Again where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
Again where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
||||||
|
|
@ -83,28 +85,44 @@ example uses the REST API.
|
||||||
from Adafruit_IO import Client
|
from Adafruit_IO import Client
|
||||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
# Send a value to a feed called 'Feed Name'.
|
# Send the value 100 to a feed called 'Foo'.
|
||||||
# Data can be of any type, string, number, hash, json.
|
aio.send('Foo', 100)
|
||||||
aio.send('Feed Name', data)
|
|
||||||
|
|
||||||
# Retrieve the most recent value from the feed 'Feed Name'.
|
# Retrieve the most recent value from the feed 'Foo'.
|
||||||
# Notice the returned object has a property called value.
|
# Access the value by reading the `value` property on the returned Data object.
|
||||||
data = aio.receive("Feed Name")
|
# Note that all values retrieved from IO are strings so you might need to convert
|
||||||
|
# them to an int or numeric type if you expect a number.
|
||||||
|
data = aio.receive('Foo')
|
||||||
print('Received value: {0}'.format(data.value))
|
print('Received value: {0}'.format(data.value))
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to be notified of feed changes immediately without polling, consider
|
If you want to be notified of feed changes immediately without polling, consider
|
||||||
using the MQTT client. See the [examples\mqtt_client.py](https://github.com/adafruit/io-client-python/blob/master/examples/mqtt_client.py) for an example of using the MQTT client.
|
using the MQTT client. See the [examples\mqtt_client.py](https://github.com/adafruit/io-client-python/blob/master/examples/mqtt_client.py) for an example of using the MQTT client.
|
||||||
|
|
||||||
|
### More Information
|
||||||
|
|
||||||
|
See the details below for more information about using the Adafruit IO python
|
||||||
|
client. You can also print out the documentation on the client and classes by
|
||||||
|
running:
|
||||||
|
|
||||||
|
```
|
||||||
|
pydoc Adafruit_IO.client
|
||||||
|
pydoc Adafruit_IO.mqtt_client
|
||||||
|
pydoc Adafruit_IO.model
|
||||||
|
pydoc Adafruit_IO.errors
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Feeds](#feeds)
|
* [Feeds](#feeds)
|
||||||
* [Creation, Retrieval, Updating](#feed-creation-retrieval-updating)
|
* [Create](#feed-creation)
|
||||||
|
* [Read](#feed-retrieval)
|
||||||
|
* [Update](#feed-updating)
|
||||||
* [Delete](#feed-deletion)
|
* [Delete](#feed-deletion)
|
||||||
* [Data](#data)
|
* [Data](#data)
|
||||||
* [Create](#data-creation)
|
* [Create](#data-creation)
|
||||||
* [Read](#data-retrieval)
|
* [Read](#data-retrieval)
|
||||||
* [Updating, Deletion](#data-updating-deletion)
|
* [Update](#data-updating)
|
||||||
|
* [Delete](#data-deletion)
|
||||||
* [Helper Methods](#helper-methods)
|
* [Helper Methods](#helper-methods)
|
||||||
* [Send](#send)
|
* [Send](#send)
|
||||||
* [Receive](#receive)
|
* [Receive](#receive)
|
||||||
|
|
@ -112,6 +130,10 @@ using the MQTT client. See the [examples\mqtt_client.py](https://github.com/ada
|
||||||
* [Previous](#previous)
|
* [Previous](#previous)
|
||||||
* [Publishing and Subscribing](#publishing-and-subscribing)
|
* [Publishing and Subscribing](#publishing-and-subscribing)
|
||||||
* [Groups](#groups)
|
* [Groups](#groups)
|
||||||
|
* [Create](#group-creation)
|
||||||
|
* [Read](#group-retrieval)
|
||||||
|
* [Update](#group-updating)
|
||||||
|
* [Delete](#group-deletion)
|
||||||
|
|
||||||
### Feeds
|
### Feeds
|
||||||
|
|
||||||
|
|
@ -120,18 +142,76 @@ that gets pushed, and you will have one feed for each type of data you send to
|
||||||
the system. You can have separate feeds for each sensor in a project, or you can
|
the system. You can have separate feeds for each sensor in a project, or you can
|
||||||
use one feed to contain JSON encoded data for all of your sensors.
|
use one feed to contain JSON encoded data for all of your sensors.
|
||||||
|
|
||||||
#### Feed Creation, Retrieval, Updating
|
#### Feed Creation
|
||||||
|
|
||||||
TODO: The python client does not currently support creating, retrieving, or
|
Create a feed by constructing a Feed instance with at least a name specified, and
|
||||||
updating a feed's metadata. See the send helper function below for creating a
|
then pass it to the `create_feed(feed)` function:
|
||||||
feed and sending it new data.
|
|
||||||
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client, Feed
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Create Feed object with name 'Foo'.
|
||||||
|
feed = Feed(name='Foo')
|
||||||
|
|
||||||
|
# Send the Feed to IO to create.
|
||||||
|
# The returned object will contain all the details about the created feed.
|
||||||
|
result = aio.create_feed(feed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that you can use the [send](#send) function to create a feed and send it a
|
||||||
|
new value in a single call. It's recommended that you use send instead of
|
||||||
|
manually constructing feed instances.
|
||||||
|
|
||||||
|
#### Feed Retrieval
|
||||||
|
|
||||||
|
You can get a list of your feeds by using the `feeds()` method which will return
|
||||||
|
a list of Feed instances:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Get list of feeds.
|
||||||
|
feeds = aio.feeds()
|
||||||
|
|
||||||
|
# Print out the feed names:
|
||||||
|
for f in feeds:
|
||||||
|
print('Feed: {0}'.format(f.name))
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively you can retrieve the metadata for a single feed by calling
|
||||||
|
`feeds(feed)` and passing the name, ID, or key of a feed to retrieve:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Get feed 'Foo'
|
||||||
|
feed = aio.feeds('Foo')
|
||||||
|
|
||||||
|
# Print out the feed metadata.
|
||||||
|
print(feed)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Feed Updating
|
||||||
|
|
||||||
|
TODO: This is not tested in the python client yet, but calling create_feed with
|
||||||
|
a Feed instance should update the feed.
|
||||||
|
|
||||||
#### Feed Deletion
|
#### Feed Deletion
|
||||||
|
|
||||||
You can delete a feed by ID, key, or name by calling `aio.delete_feed(feed)`.
|
You can delete a feed by ID, key, or name by calling `delete_feed(feed)`.
|
||||||
ALL data in the feed will be deleted after calling this API!
|
ALL data in the feed will be deleted after calling this API!
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
# Delete the feed with name 'Test'.
|
# Delete the feed with name 'Test'.
|
||||||
aio.delete_feed('Test')
|
aio.delete_feed('Test')
|
||||||
```
|
```
|
||||||
|
|
@ -145,47 +225,76 @@ and selecting certain pieces of data.
|
||||||
#### Data Creation
|
#### Data Creation
|
||||||
|
|
||||||
Data can be created [after you create a feed](#data-creation), by using the
|
Data can be created [after you create a feed](#data-creation), by using the
|
||||||
`aio.create_data(feed, data)` method and passing it a new Data instance with
|
`create_data(feed, data)` method and passing it a new Data instance a value.
|
||||||
a value set. See the [send function](#send) for a simpler and recommended way
|
See also the [send function](#send) for a simpler way to add a value to feed and
|
||||||
of adding a new value to a feed.
|
create the feed in one call.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client, Data
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
# Create a data item with value 10 in the 'Test' feed.
|
# Create a data item with value 10 in the 'Test' feed.
|
||||||
data = Adafruit_IO.Data(value=10)
|
data = Data(value=10)
|
||||||
aio.create_stream('Test', data)
|
aio.create_data('Test', data)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Data Retrieval
|
#### Data Retrieval
|
||||||
|
|
||||||
You can get all of the data for a feed by using the `aio.data(feed)` method. The
|
You can get all of the data for a feed by using the `data(feed)` method. The
|
||||||
result will be an array of all feed data, each returned as an instance of the
|
result will be an array of all feed data, each returned as an instance of the
|
||||||
Data class. Use the value property on each Data instance to get the data value.
|
Data class. Use the value property on each Data instance to get the data value,
|
||||||
|
and remember values are always returned as strings (so you might need to convert
|
||||||
|
to an int or number if you expect a numeric value).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
# Get an array of all data from feed 'Test'
|
# Get an array of all data from feed 'Test'
|
||||||
data = aio.streams('Test')
|
data = aio.data('Test')
|
||||||
|
|
||||||
# Print out all the results.
|
# Print out all the results.
|
||||||
for d in data:
|
for d in data:
|
||||||
print('Data value: {0}'.format(d.value))
|
print('Data value: {0}'.format(d.value))
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also get a specific value by ID by using the `aio.feeds(feed, data_id)`
|
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
|
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
|
if it exists in the feed. The returned object will be an instance of the Data
|
||||||
class.
|
class.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
# Get a specific value by id.
|
# Get a specific value by id.
|
||||||
# This example assumes 1 is a valid data ID in the 'Test' feed
|
# This example assumes 1 is a valid data ID in the 'Test' feed
|
||||||
data = aio.feeds('Test', 1)
|
data = aio.feeds('Test', 1)
|
||||||
|
|
||||||
# Print the value.
|
# Print the value.
|
||||||
print('Data value: {0}'.format(data.value))
|
print('Data value: {0}'.format(data.value))
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Data Updating, Deletion
|
#### Data Updating
|
||||||
|
|
||||||
TODO: The python client does not currently support updating or deleting feed
|
TODO: This is not tested in the python client, but calling create_data with a
|
||||||
data.
|
Data instance should update it.
|
||||||
|
|
||||||
|
#### Data Deletion
|
||||||
|
|
||||||
|
Values can be deleted by using the `delete(feed, data_id)` method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Delete a data value from feed 'Test' with ID 1.
|
||||||
|
data = aio.delete('Test', 1)
|
||||||
|
```
|
||||||
|
|
||||||
#### Helper Methods
|
#### Helper Methods
|
||||||
|
|
||||||
|
|
@ -193,46 +302,72 @@ There are a few helper methods that can make interacting with data a bit easier.
|
||||||
|
|
||||||
##### Send
|
##### Send
|
||||||
|
|
||||||
You can use the `aio.send(feed_name, value)` method to append a new value to a
|
You can use the `send(feed_name, value)` method to append a new value to a
|
||||||
feed in one call. If the specified feed does not exist it will automatically be
|
feed in one call. If the specified feed does not exist it will automatically be
|
||||||
created. This is the recommended way to send data to Adafruit IO from the Python
|
created. This is the recommended way to send data to Adafruit IO from the Python
|
||||||
client.
|
REST client.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Add the value 98.6 to the feed 'Test Send Data'.
|
# Import library and create instance of REST client.
|
||||||
aio.send('Test Send Data', 98.6)
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Add the value 98.6 to the feed 'Temperature'.
|
||||||
|
aio.send('Temperature', 98.6)
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Receive
|
##### Receive
|
||||||
|
|
||||||
You can get the last inserted value by using the `aio.receive(feed)` method.
|
You can get the last inserted value by using the `receive(feed)` method.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Get the last value of the temperature feed.
|
||||||
data = aio.receive('Test')
|
data = aio.receive('Test')
|
||||||
# Print the value.
|
|
||||||
print('Data value: {0}'.format(data))
|
# Print the value and a message if it's over 100. Notice that the value is
|
||||||
|
# converted from string to int because it always comes back as a string from IO.
|
||||||
|
temp = int(data.value)
|
||||||
|
print('Temperature: {0}'.format(temp))
|
||||||
|
if temp > 100:
|
||||||
|
print 'Hot enough for you?'
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Next
|
##### Next
|
||||||
|
|
||||||
You can get the first inserted value that has not been processed by using the
|
You can get the first inserted value that has not been processed (read) by using
|
||||||
`aio.receive_next(feed)` method.
|
the `receive_next(feed)` method.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Get next unread value from feed 'Test'.
|
||||||
data = aio.receive_next('Test')
|
data = aio.receive_next('Test')
|
||||||
|
|
||||||
# Print the value.
|
# Print the value.
|
||||||
print('Data value: {0}'.format(data))
|
print('Data value: {0}'.format(data.value))
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Previous
|
##### Previous
|
||||||
|
|
||||||
You can get the the last record that has been processed by using the
|
You can get the the last record that has been processed (read) by using the
|
||||||
`aio.receive_previous(feed)` method.
|
`receive_previous(feed)` method.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Get previous read value from feed 'Test'.
|
||||||
data = aio.receive_previous('Test')
|
data = aio.receive_previous('Test')
|
||||||
|
|
||||||
# Print the value.
|
# Print the value.
|
||||||
print('Data value: {0}'.format(data))
|
print('Data value: {0}'.format(data.value))
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Publishing and Subscribing
|
#### Publishing and Subscribing
|
||||||
|
|
@ -240,14 +375,70 @@ print('Data value: {0}'.format(data))
|
||||||
You can get a readable stream of live data from your feed using the included
|
You can get a readable stream of live data from your feed using the included
|
||||||
MQTT client class.
|
MQTT client class.
|
||||||
|
|
||||||
TBD: Document using the MQTT client.
|
TBD: Document using the MQTT client. For now see the [examples\mqtt_client.py](https://github.com/adafruit/io-client-python/blob/master/examples/mqtt_client.py) example which is fully documented with comments.
|
||||||
|
|
||||||
### Groups
|
### Groups
|
||||||
|
|
||||||
Groups allow you to update and retrieve multiple feeds with one request. You can
|
Groups allow you to update and retrieve multiple feeds with one request. You can
|
||||||
add feeds to multiple groups.
|
add feeds to multiple groups.
|
||||||
|
|
||||||
TBD
|
#### Group Creation
|
||||||
|
|
||||||
|
TBD: Currently group creation doesn't work with the APIs. Groups must be created
|
||||||
|
in the UI.
|
||||||
|
|
||||||
|
#### Group Retrieval
|
||||||
|
|
||||||
|
You can get a list of your groups by using the `groups()` method. This will
|
||||||
|
return a list of Group instances. Each Group instance has metadata about the
|
||||||
|
group, including a `feeds` property which is a tuple of all feeds in the group.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Get list of groups.
|
||||||
|
groups = aio.groups()
|
||||||
|
|
||||||
|
# Print the group names and number of feeds in the group.
|
||||||
|
for g in groups:
|
||||||
|
print('Group {0} has {1} feed(s).'.format(g.name, len(g.feeds)))
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also get a specific group by ID, key, or name by using the
|
||||||
|
`groups(group)` method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Get group called 'GroupTest'.
|
||||||
|
group = aio.groups('GroupTest')
|
||||||
|
|
||||||
|
# Print the group name and number of feeds in the group.
|
||||||
|
print('Group {0} has {1} feed(s).'.format(group.name, len(group.feeds)))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Group Updating
|
||||||
|
|
||||||
|
TBD This is not tested in the python client yet, but calling create_group should
|
||||||
|
update a group.
|
||||||
|
|
||||||
|
#### Group Deletion
|
||||||
|
|
||||||
|
You can delete a group by ID, key, or name by using the `delete_group(group)`
|
||||||
|
method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Import library and create instance of REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||||
|
|
||||||
|
# Delete group called 'GroupTest'.
|
||||||
|
aio.delete_group('GroupTest')
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
# Example of using the MQTT client class to subscribe to and publish feed values.
|
# Example of using the MQTT client class to subscribe to and publish feed values.
|
||||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
# Author: Tony DiCola
|
||||||
|
|
||||||
# Import standard python modules.
|
# Import standard python modules.
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Import Adafruit IO client.
|
# Import Adafruit IO MQTT client.
|
||||||
import Adafruit_IO
|
from Adafruit_IO import MQTTClient
|
||||||
|
|
||||||
|
|
||||||
# Set to your Adafruit IO key.
|
# Set to your Adafruit IO key.
|
||||||
|
|
@ -37,7 +37,7 @@ def message(client, feed_id, payload):
|
||||||
|
|
||||||
|
|
||||||
# Create an MQTT client instance.
|
# Create an MQTT client instance.
|
||||||
client = Adafruit_IO.MQTTClient(ADAFRUIT_IO_KEY)
|
client = MQTTClient(ADAFRUIT_IO_KEY)
|
||||||
|
|
||||||
# Setup the callback functions defined above.
|
# Setup the callback functions defined above.
|
||||||
client.on_connect = connected
|
client.on_connect = connected
|
||||||
|
|
|
||||||
32
examples/simple.py
Normal file
32
examples/simple.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Simple example of sending and receiving values from Adafruit IO with the REST
|
||||||
|
# API client.
|
||||||
|
# Author: Tony DiCola
|
||||||
|
|
||||||
|
# Import Adafruit IO REST client.
|
||||||
|
from Adafruit_IO import Client
|
||||||
|
|
||||||
|
# Set to your Adafruit IO key.
|
||||||
|
ADAFRUIT_IO_KEY = 'YOUR ADAFRUIT IO KEY'
|
||||||
|
|
||||||
|
# Create an instance of the REST client.
|
||||||
|
aio = Client(ADAFRUIT_IO_KEY)
|
||||||
|
|
||||||
|
# Send a value to the feed 'Test'. This will create the feed if it doesn't
|
||||||
|
# exist already.
|
||||||
|
aio.send('Test', 42)
|
||||||
|
|
||||||
|
# Send a string value 'bar' to the feed 'Foo', again creating it if it doesn't
|
||||||
|
# exist already.
|
||||||
|
aio.send('Foo', 'bar')
|
||||||
|
|
||||||
|
# Now read the most recent value from the feed 'Test'. Notice that it comes
|
||||||
|
# back as a string and should be converted to an int if performing calculations
|
||||||
|
# on it.
|
||||||
|
data = aio.receive('Test')
|
||||||
|
print('Retrieved value from Test has attributes: {0}'.format(data))
|
||||||
|
print('Latest value from Test: {0}'.format(data.value))
|
||||||
|
|
||||||
|
# Finally read the most revent value from feed 'Foo'.
|
||||||
|
data = aio.receive('Foo')
|
||||||
|
print('Retrieved value from Test has attributes: {0}'.format(data))
|
||||||
|
print('Latest value from Test: {0}'.format(data.value))
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
from Adafruit_IO import Client
|
|
||||||
io = Client('e2b0fac48ae32f324df4aa05247c16e991494b08')
|
|
||||||
|
|
||||||
r = io.send("Test Python", 12)
|
|
||||||
print r
|
|
||||||
|
|
||||||
r = io.receive("Test Python")
|
|
||||||
print r
|
|
||||||
|
|
||||||
r = io.receive_next("Test Python")
|
|
||||||
print r
|
|
||||||
332
ez_setup.py
Normal file
332
ez_setup.py
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Bootstrap setuptools installation
|
||||||
|
|
||||||
|
To use setuptools in your package's setup.py, include this
|
||||||
|
file in the same directory and add this to the top of your setup.py::
|
||||||
|
|
||||||
|
from ez_setup import use_setuptools
|
||||||
|
use_setuptools()
|
||||||
|
|
||||||
|
To require a specific version of setuptools, set a download
|
||||||
|
mirror, or use an alternate download directory, simply supply
|
||||||
|
the appropriate options to ``use_setuptools()``.
|
||||||
|
|
||||||
|
This file can also be run as a script to install or upgrade setuptools.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
import optparse
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
import textwrap
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
from distutils import log
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib.request import urlopen
|
||||||
|
except ImportError:
|
||||||
|
from urllib2 import urlopen
|
||||||
|
|
||||||
|
try:
|
||||||
|
from site import USER_SITE
|
||||||
|
except ImportError:
|
||||||
|
USER_SITE = None
|
||||||
|
|
||||||
|
DEFAULT_VERSION = "4.0.1"
|
||||||
|
DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
|
||||||
|
|
||||||
|
def _python_cmd(*args):
|
||||||
|
"""
|
||||||
|
Return True if the command succeeded.
|
||||||
|
"""
|
||||||
|
args = (sys.executable,) + args
|
||||||
|
return subprocess.call(args) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _install(archive_filename, install_args=()):
|
||||||
|
with archive_context(archive_filename):
|
||||||
|
# installing
|
||||||
|
log.warn('Installing Setuptools')
|
||||||
|
if not _python_cmd('setup.py', 'install', *install_args):
|
||||||
|
log.warn('Something went wrong during the installation.')
|
||||||
|
log.warn('See the error message above.')
|
||||||
|
# exitcode will be 2
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def _build_egg(egg, archive_filename, to_dir):
|
||||||
|
with archive_context(archive_filename):
|
||||||
|
# building an egg
|
||||||
|
log.warn('Building a Setuptools egg in %s', to_dir)
|
||||||
|
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
|
||||||
|
# returning the result
|
||||||
|
log.warn(egg)
|
||||||
|
if not os.path.exists(egg):
|
||||||
|
raise IOError('Could not build the egg.')
|
||||||
|
|
||||||
|
|
||||||
|
class ContextualZipFile(zipfile.ZipFile):
|
||||||
|
"""
|
||||||
|
Supplement ZipFile class to support context manager for Python 2.6
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Construct a ZipFile or ContextualZipFile as appropriate
|
||||||
|
"""
|
||||||
|
if hasattr(zipfile.ZipFile, '__exit__'):
|
||||||
|
return zipfile.ZipFile(*args, **kwargs)
|
||||||
|
return super(ContextualZipFile, cls).__new__(cls)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def archive_context(filename):
|
||||||
|
# extracting the archive
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
log.warn('Extracting in %s', tmpdir)
|
||||||
|
old_wd = os.getcwd()
|
||||||
|
try:
|
||||||
|
os.chdir(tmpdir)
|
||||||
|
with ContextualZipFile(filename) as archive:
|
||||||
|
archive.extractall()
|
||||||
|
|
||||||
|
# going in the directory
|
||||||
|
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||||
|
os.chdir(subdir)
|
||||||
|
log.warn('Now working in %s', subdir)
|
||||||
|
yield
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.chdir(old_wd)
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
def _do_download(version, download_base, to_dir, download_delay):
|
||||||
|
egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
|
||||||
|
% (version, sys.version_info[0], sys.version_info[1]))
|
||||||
|
if not os.path.exists(egg):
|
||||||
|
archive = download_setuptools(version, download_base,
|
||||||
|
to_dir, download_delay)
|
||||||
|
_build_egg(egg, archive, to_dir)
|
||||||
|
sys.path.insert(0, egg)
|
||||||
|
|
||||||
|
# Remove previously-imported pkg_resources if present (see
|
||||||
|
# https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
|
||||||
|
if 'pkg_resources' in sys.modules:
|
||||||
|
del sys.modules['pkg_resources']
|
||||||
|
|
||||||
|
import setuptools
|
||||||
|
setuptools.bootstrap_install_from = egg
|
||||||
|
|
||||||
|
|
||||||
|
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||||
|
to_dir=os.curdir, download_delay=15):
|
||||||
|
to_dir = os.path.abspath(to_dir)
|
||||||
|
rep_modules = 'pkg_resources', 'setuptools'
|
||||||
|
imported = set(sys.modules).intersection(rep_modules)
|
||||||
|
try:
|
||||||
|
import pkg_resources
|
||||||
|
except ImportError:
|
||||||
|
return _do_download(version, download_base, to_dir, download_delay)
|
||||||
|
try:
|
||||||
|
pkg_resources.require("setuptools>=" + version)
|
||||||
|
return
|
||||||
|
except pkg_resources.DistributionNotFound:
|
||||||
|
return _do_download(version, download_base, to_dir, download_delay)
|
||||||
|
except pkg_resources.VersionConflict as VC_err:
|
||||||
|
if imported:
|
||||||
|
msg = textwrap.dedent("""
|
||||||
|
The required version of setuptools (>={version}) is not available,
|
||||||
|
and can't be installed while this script is running. Please
|
||||||
|
install a more recent version first, using
|
||||||
|
'easy_install -U setuptools'.
|
||||||
|
|
||||||
|
(Currently using {VC_err.args[0]!r})
|
||||||
|
""").format(VC_err=VC_err, version=version)
|
||||||
|
sys.stderr.write(msg)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
# otherwise, reload ok
|
||||||
|
del pkg_resources, sys.modules['pkg_resources']
|
||||||
|
return _do_download(version, download_base, to_dir, download_delay)
|
||||||
|
|
||||||
|
def _clean_check(cmd, target):
|
||||||
|
"""
|
||||||
|
Run the command to download target. If the command fails, clean up before
|
||||||
|
re-raising the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
if os.access(target, os.F_OK):
|
||||||
|
os.unlink(target)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def download_file_powershell(url, target):
|
||||||
|
"""
|
||||||
|
Download the file at url to target using Powershell (which will validate
|
||||||
|
trust). Raise an exception if the command cannot complete.
|
||||||
|
"""
|
||||||
|
target = os.path.abspath(target)
|
||||||
|
ps_cmd = (
|
||||||
|
"[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
|
||||||
|
"[System.Net.CredentialCache]::DefaultCredentials; "
|
||||||
|
"(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)"
|
||||||
|
% vars()
|
||||||
|
)
|
||||||
|
cmd = [
|
||||||
|
'powershell',
|
||||||
|
'-Command',
|
||||||
|
ps_cmd,
|
||||||
|
]
|
||||||
|
_clean_check(cmd, target)
|
||||||
|
|
||||||
|
def has_powershell():
|
||||||
|
if platform.system() != 'Windows':
|
||||||
|
return False
|
||||||
|
cmd = ['powershell', '-Command', 'echo test']
|
||||||
|
with open(os.path.devnull, 'wb') as devnull:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
download_file_powershell.viable = has_powershell
|
||||||
|
|
||||||
|
def download_file_curl(url, target):
|
||||||
|
cmd = ['curl', url, '--silent', '--output', target]
|
||||||
|
_clean_check(cmd, target)
|
||||||
|
|
||||||
|
def has_curl():
|
||||||
|
cmd = ['curl', '--version']
|
||||||
|
with open(os.path.devnull, 'wb') as devnull:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
download_file_curl.viable = has_curl
|
||||||
|
|
||||||
|
def download_file_wget(url, target):
|
||||||
|
cmd = ['wget', url, '--quiet', '--output-document', target]
|
||||||
|
_clean_check(cmd, target)
|
||||||
|
|
||||||
|
def has_wget():
|
||||||
|
cmd = ['wget', '--version']
|
||||||
|
with open(os.path.devnull, 'wb') as devnull:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
download_file_wget.viable = has_wget
|
||||||
|
|
||||||
|
def download_file_insecure(url, target):
|
||||||
|
"""
|
||||||
|
Use Python to download the file, even though it cannot authenticate the
|
||||||
|
connection.
|
||||||
|
"""
|
||||||
|
src = urlopen(url)
|
||||||
|
try:
|
||||||
|
# Read all the data in one block.
|
||||||
|
data = src.read()
|
||||||
|
finally:
|
||||||
|
src.close()
|
||||||
|
|
||||||
|
# Write all the data in one block to avoid creating a partial file.
|
||||||
|
with open(target, "wb") as dst:
|
||||||
|
dst.write(data)
|
||||||
|
|
||||||
|
download_file_insecure.viable = lambda: True
|
||||||
|
|
||||||
|
def get_best_downloader():
|
||||||
|
downloaders = (
|
||||||
|
download_file_powershell,
|
||||||
|
download_file_curl,
|
||||||
|
download_file_wget,
|
||||||
|
download_file_insecure,
|
||||||
|
)
|
||||||
|
viable_downloaders = (dl for dl in downloaders if dl.viable())
|
||||||
|
return next(viable_downloaders, None)
|
||||||
|
|
||||||
|
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||||
|
to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader):
|
||||||
|
"""
|
||||||
|
Download setuptools from a specified location and return its filename
|
||||||
|
|
||||||
|
`version` should be a valid setuptools version number that is available
|
||||||
|
as an egg for download under the `download_base` URL (which should end
|
||||||
|
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
||||||
|
`delay` is the number of seconds to pause before an actual download
|
||||||
|
attempt.
|
||||||
|
|
||||||
|
``downloader_factory`` should be a function taking no arguments and
|
||||||
|
returning a function for downloading a URL to a target.
|
||||||
|
"""
|
||||||
|
# making sure we use the absolute path
|
||||||
|
to_dir = os.path.abspath(to_dir)
|
||||||
|
zip_name = "setuptools-%s.zip" % version
|
||||||
|
url = download_base + zip_name
|
||||||
|
saveto = os.path.join(to_dir, zip_name)
|
||||||
|
if not os.path.exists(saveto): # Avoid repeated downloads
|
||||||
|
log.warn("Downloading %s", url)
|
||||||
|
downloader = downloader_factory()
|
||||||
|
downloader(url, saveto)
|
||||||
|
return os.path.realpath(saveto)
|
||||||
|
|
||||||
|
def _build_install_args(options):
|
||||||
|
"""
|
||||||
|
Build the arguments to 'python setup.py install' on the setuptools package
|
||||||
|
"""
|
||||||
|
return ['--user'] if options.user_install else []
|
||||||
|
|
||||||
|
def _parse_args():
|
||||||
|
"""
|
||||||
|
Parse the command line for options
|
||||||
|
"""
|
||||||
|
parser = optparse.OptionParser()
|
||||||
|
parser.add_option(
|
||||||
|
'--user', dest='user_install', action='store_true', default=False,
|
||||||
|
help='install in user site package (requires Python 2.6 or later)')
|
||||||
|
parser.add_option(
|
||||||
|
'--download-base', dest='download_base', metavar="URL",
|
||||||
|
default=DEFAULT_URL,
|
||||||
|
help='alternative URL from where to download the setuptools package')
|
||||||
|
parser.add_option(
|
||||||
|
'--insecure', dest='downloader_factory', action='store_const',
|
||||||
|
const=lambda: download_file_insecure, default=get_best_downloader,
|
||||||
|
help='Use internal, non-validating downloader'
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
'--version', help="Specify which version to download",
|
||||||
|
default=DEFAULT_VERSION,
|
||||||
|
)
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
# positional arguments are ignored
|
||||||
|
return options
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Install or upgrade setuptools and EasyInstall"""
|
||||||
|
options = _parse_args()
|
||||||
|
archive = download_setuptools(
|
||||||
|
version=options.version,
|
||||||
|
download_base=options.download_base,
|
||||||
|
downloader_factory=options.downloader_factory,
|
||||||
|
)
|
||||||
|
return _install(archive, _build_install_args(options))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
7
setup.py
7
setup.py
|
|
@ -1,8 +1,10 @@
|
||||||
|
from ez_setup import use_setuptools
|
||||||
|
use_setuptools()
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='adafruit-io',
|
name='adafruit-io',
|
||||||
version='0.0.1',
|
version='0.9.0',
|
||||||
author='Justin Cooper',
|
author='Justin Cooper',
|
||||||
author_email='justin@adafruit.com',
|
author_email='justin@adafruit.com',
|
||||||
packages=['Adafruit_IO'],
|
packages=['Adafruit_IO'],
|
||||||
|
|
@ -11,6 +13,7 @@ setup(
|
||||||
description='IO Client library for io.adafruit.com',
|
description='IO Client library for io.adafruit.com',
|
||||||
long_description=open('README.md').read(),
|
long_description=open('README.md').read(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"apiclient >= 1.0.2"
|
"requests",
|
||||||
|
"paho-mqtt"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,25 @@
|
||||||
# Base testcase class with functions and state available to all tests.
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
# Author: Tony DiCola
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import Adafruit_IO
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
class IOTestCase(unittest.TestCase):
|
class IOTestCase(unittest.TestCase):
|
||||||
|
|
@ -18,13 +33,3 @@ class IOTestCase(unittest.TestCase):
|
||||||
raise RuntimeError("ADAFRUIT_IO_KEY environment variable must be " \
|
raise RuntimeError("ADAFRUIT_IO_KEY environment variable must be " \
|
||||||
"set with valid Adafruit IO key to run this test!")
|
"set with valid Adafruit IO key to run this test!")
|
||||||
return key
|
return key
|
||||||
|
|
||||||
def ensure_feed_deleted(self, client, feed):
|
|
||||||
"""Delete the provided feed if it exists. Does nothing if the feed
|
|
||||||
doesn't exist.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
client.delete_feed(feed)
|
|
||||||
except Adafruit_IO.RequestError:
|
|
||||||
# Swallow the error if the feed doesn't exist.
|
|
||||||
pass
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
# Test REST client.
|
# Test REST client.
|
||||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||||
import time
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from Adafruit_IO import Client, Data, Feed, Group, RequestError
|
||||||
|
|
||||||
import Adafruit_IO
|
|
||||||
import base
|
import base
|
||||||
|
|
||||||
# Turn this on to see responses of requests from urllib.
|
|
||||||
#import logging
|
# Default config for tests to run against real Adafruit IO service with no proxy.
|
||||||
#logging.basicConfig(level=logging.DEBUG)
|
BASE_URL = 'https://io.adafruit.com/'
|
||||||
|
PROXIES = None
|
||||||
|
|
||||||
|
# Config to run tests against real Adafruit IO service over non-SSL and with a
|
||||||
|
# a proxy running on localhost 8888 (good for getting traces with fiddler).
|
||||||
|
#BASE_URL = 'http://io.adafruit.com/'
|
||||||
|
#PROXIES = {'http': 'http://localhost:8888/'}
|
||||||
|
|
||||||
|
|
||||||
class TestClient(base.IOTestCase):
|
class TestClient(base.IOTestCase):
|
||||||
|
|
||||||
|
|
@ -16,24 +25,39 @@ class TestClient(base.IOTestCase):
|
||||||
#def tearDown(self):
|
#def tearDown(self):
|
||||||
# time.sleep(30.0)
|
# time.sleep(30.0)
|
||||||
|
|
||||||
|
def get_client(self):
|
||||||
|
# Construct an Adafruit IO REST client and return it.
|
||||||
|
return Client(self.get_test_key(), proxies=PROXIES, base_url=BASE_URL)
|
||||||
|
|
||||||
|
def ensure_feed_deleted(self, client, feed):
|
||||||
|
# Delete the specified feed if it exists.
|
||||||
|
try:
|
||||||
|
client.delete_feed(feed)
|
||||||
|
except RequestError:
|
||||||
|
# Swallow the error if the feed doesn't exist.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ensure_group_deleted(self, client, group):
|
||||||
|
# Delete the specified group if it exists.
|
||||||
|
try:
|
||||||
|
client.delete_group(group)
|
||||||
|
except RequestError:
|
||||||
|
# Swallow the error if the group doesn't exist.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def empty_feed(self, client, feed):
|
||||||
|
# Remove all the data from a specified feed (but don't delete the feed).
|
||||||
|
data = client.data(feed)
|
||||||
|
for d in data:
|
||||||
|
client.delete(feed, d.id)
|
||||||
|
|
||||||
def test_set_key(self):
|
def test_set_key(self):
|
||||||
key = "unique_key_id"
|
key = "unique_key_id"
|
||||||
io = Adafruit_IO.Client(key)
|
io = Client(key)
|
||||||
self.assertEqual(key, io.key)
|
self.assertEqual(key, io.key)
|
||||||
|
|
||||||
def test_delete_feed(self):
|
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
|
||||||
io.send('TestFeed', 'foo') # Make sure a feed called TestFeed exists.
|
|
||||||
io.delete_feed('TestFeed')
|
|
||||||
self.assertRaises(Adafruit_IO.RequestError, io.receive, 'TestFeed')
|
|
||||||
|
|
||||||
def test_delete_nonexistant_feed_fails(self):
|
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
|
||||||
self.ensure_feed_deleted(io, 'TestFeed')
|
|
||||||
self.assertRaises(Adafruit_IO.RequestError, io.delete_feed, 'TestFeed')
|
|
||||||
|
|
||||||
def test_send_and_receive(self):
|
def test_send_and_receive(self):
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
io = self.get_client()
|
||||||
self.ensure_feed_deleted(io, 'TestFeed')
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
response = io.send('TestFeed', 'foo')
|
response = io.send('TestFeed', 'foo')
|
||||||
self.assertEqual(response.value, 'foo')
|
self.assertEqual(response.value, 'foo')
|
||||||
|
|
@ -41,22 +65,28 @@ class TestClient(base.IOTestCase):
|
||||||
self.assertEqual(data.value, 'foo')
|
self.assertEqual(data.value, 'foo')
|
||||||
|
|
||||||
def test_receive_next(self):
|
def test_receive_next(self):
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
io = self.get_client()
|
||||||
self.ensure_feed_deleted(io, 'TestFeed')
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
io.send('TestFeed', 1)
|
io.send('TestFeed', 1)
|
||||||
data = io.receive_next('TestFeed')
|
data = io.receive_next('TestFeed')
|
||||||
self.assertEqual(int(data.value), 1)
|
self.assertEqual(int(data.value), 1)
|
||||||
|
|
||||||
|
# BUG: Previous jumps too far back: https://github.com/adafruit/io/issues/55
|
||||||
|
@unittest.expectedFailure
|
||||||
def test_receive_previous(self):
|
def test_receive_previous(self):
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
io = self.get_client()
|
||||||
self.ensure_feed_deleted(io, 'TestFeed')
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
io.send('TestFeed', 1)
|
io.send('TestFeed', 1)
|
||||||
io.receive_next('TestFeed')
|
io.send('TestFeed', 2)
|
||||||
|
io.receive_next('TestFeed') # Receive 1
|
||||||
|
io.receive_next('TestFeed') # Receive 2
|
||||||
|
data = io.receive_previous('TestFeed')
|
||||||
|
self.assertEqual(int(data.value), 2)
|
||||||
data = io.receive_previous('TestFeed')
|
data = io.receive_previous('TestFeed')
|
||||||
self.assertEqual(int(data.value), 1)
|
self.assertEqual(int(data.value), 1)
|
||||||
|
|
||||||
def test_data_on_feed_returns_all_data(self):
|
def test_data_on_feed_returns_all_data(self):
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
io = self.get_client()
|
||||||
self.ensure_feed_deleted(io, 'TestFeed')
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
io.send('TestFeed', 1)
|
io.send('TestFeed', 1)
|
||||||
io.send('TestFeed', 2)
|
io.send('TestFeed', 2)
|
||||||
|
|
@ -66,7 +96,7 @@ class TestClient(base.IOTestCase):
|
||||||
self.assertEqual(int(result[1].value), 2)
|
self.assertEqual(int(result[1].value), 2)
|
||||||
|
|
||||||
def test_data_on_feed_and_data_id_returns_data(self):
|
def test_data_on_feed_and_data_id_returns_data(self):
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
io = self.get_client()
|
||||||
self.ensure_feed_deleted(io, 'TestFeed')
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
data = io.send('TestFeed', 1)
|
data = io.send('TestFeed', 1)
|
||||||
result = io.data('TestFeed', data.id)
|
result = io.data('TestFeed', data.id)
|
||||||
|
|
@ -74,9 +104,135 @@ class TestClient(base.IOTestCase):
|
||||||
self.assertEqual(int(data.value), int(result.value))
|
self.assertEqual(int(data.value), int(result.value))
|
||||||
|
|
||||||
def test_create_data(self):
|
def test_create_data(self):
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
io = self.get_client()
|
||||||
self.ensure_feed_deleted(io, 'TestFeed')
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
io.send('TestFeed', 1) # Make sure TestFeed exists.
|
io.send('TestFeed', 1) # Make sure TestFeed exists.
|
||||||
data = Adafruit_IO.Data(value=42)
|
data = Data(value=42)
|
||||||
result = io.create_data('TestFeed', data)
|
result = io.create_data('TestFeed', data)
|
||||||
self.assertEqual(int(result.value), 42)
|
self.assertEqual(int(result.value), 42)
|
||||||
|
|
||||||
|
def test_create_feed(self):
|
||||||
|
io = self.get_client()
|
||||||
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
|
feed = Feed(name='TestFeed')
|
||||||
|
result = io.create_feed(feed)
|
||||||
|
self.assertEqual(result.name, 'TestFeed')
|
||||||
|
|
||||||
|
def test_feeds_returns_all_feeds(self):
|
||||||
|
io = self.get_client()
|
||||||
|
io.send('TestFeed', 1) # Make sure TestFeed exists.
|
||||||
|
feeds = io.feeds()
|
||||||
|
self.assertGreaterEqual(len(feeds), 1)
|
||||||
|
names = set(map(lambda x: x.name, feeds))
|
||||||
|
self.assertTrue('TestFeed' in names)
|
||||||
|
|
||||||
|
def test_feeds_returns_requested_feed(self):
|
||||||
|
io = self.get_client()
|
||||||
|
io.send('TestFeed', 1) # Make sure TestFeed exists.
|
||||||
|
result = io.feeds('TestFeed')
|
||||||
|
self.assertEqual(result.name, 'TestFeed')
|
||||||
|
self.assertEqual(int(result.last_value), 1)
|
||||||
|
|
||||||
|
def test_delete_feed(self):
|
||||||
|
io = self.get_client()
|
||||||
|
io.send('TestFeed', 'foo') # Make sure a feed called TestFeed exists.
|
||||||
|
io.delete_feed('TestFeed')
|
||||||
|
self.assertRaises(RequestError, io.receive, 'TestFeed')
|
||||||
|
|
||||||
|
def test_delete_nonexistant_feed_fails(self):
|
||||||
|
io = self.get_client()
|
||||||
|
self.ensure_feed_deleted(io, 'TestFeed')
|
||||||
|
self.assertRaises(RequestError, io.delete_feed, 'TestFeed')
|
||||||
|
|
||||||
|
# NOTE: The group tests require some manual setup once before they can run.
|
||||||
|
# Log in to io.adafruit.com and create two feeds (called "GroupFeed1" and
|
||||||
|
# "GroupFeed2" by default). Then add those feeds to a new Group called
|
||||||
|
# "GroupTest". This only has to be done once for your account. Once group
|
||||||
|
# creation is supported by the API this can be refactored to dynamically
|
||||||
|
# create the group under test.
|
||||||
|
def test_send_and_receive_group(self):
|
||||||
|
io = self.get_client()
|
||||||
|
self.empty_feed(io, 'GroupTest1')
|
||||||
|
self.empty_feed(io, 'GroupTest2')
|
||||||
|
response = io.send_group('GroupTest', {'GroupTest1': 1, 'GroupTest2': 2})
|
||||||
|
self.assertEqual(len(response.feeds), 2)
|
||||||
|
self.assertSetEqual(set(map(lambda x: int(x.stream.value), response.feeds)),
|
||||||
|
set([1, 2])) # Compare sets because order might differ.
|
||||||
|
#data = io.receive_group('GroupTest') # Group get by name currently broken.
|
||||||
|
data = io.receive_group(response.id)
|
||||||
|
self.assertEqual(len(data.feeds), 2)
|
||||||
|
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||||
|
set([1, 2]))
|
||||||
|
|
||||||
|
# BUG: Next doesn't return expected metadata: https://github.com/adafruit/io/issues/57
|
||||||
|
@unittest.expectedFailure
|
||||||
|
def test_receive_next_group(self):
|
||||||
|
io = self.get_client()
|
||||||
|
self.empty_feed(io, 'GroupTest1')
|
||||||
|
self.empty_feed(io, 'GroupTest2')
|
||||||
|
response = io.send_group('GroupTest', {'GroupTest1': 42, 'GroupTest2': 99})
|
||||||
|
data = io.receive_next_group(response.id)
|
||||||
|
self.assertEqual(len(data.feeds), 2)
|
||||||
|
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||||
|
set([42, 99]))
|
||||||
|
|
||||||
|
# BUG: Previous jumps too far back: https://github.com/adafruit/io/issues/55
|
||||||
|
@unittest.expectedFailure
|
||||||
|
def test_receive_previous_group(self):
|
||||||
|
io = self.get_client()
|
||||||
|
self.empty_feed(io, 'GroupTest1')
|
||||||
|
self.empty_feed(io, 'GroupTest2')
|
||||||
|
response = io.send_group('GroupTest', {'GroupTest1': 10, 'GroupTest2': 20})
|
||||||
|
response = io.send_group('GroupTest', {'GroupTest1': 30, 'GroupTest2': 40})
|
||||||
|
io.receive_next_group(response.id) # Receive 10, 20
|
||||||
|
io.receive_next_group(response.id) # Receive 30, 40
|
||||||
|
data = io.receive_previous_group(response.id)
|
||||||
|
self.assertEqual(len(data.feeds), 2)
|
||||||
|
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||||
|
set([30, 40]))
|
||||||
|
data = io.receive_previous_group(response.id)
|
||||||
|
self.assertEqual(len(data.feeds), 2)
|
||||||
|
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||||
|
set([10, 20]))
|
||||||
|
|
||||||
|
def test_groups_returns_all_groups(self):
|
||||||
|
io = self.get_client()
|
||||||
|
groups = io.groups()
|
||||||
|
self.assertGreaterEqual(len(groups), 1)
|
||||||
|
names = set(map(lambda x: x.name, groups))
|
||||||
|
self.assertTrue('GroupTest' in names)
|
||||||
|
|
||||||
|
def test_groups_retrieves_requested_group(self):
|
||||||
|
io = self.get_client()
|
||||||
|
response = io.groups('GroupTest')
|
||||||
|
self.assertEqual(response.name, 'GroupTest')
|
||||||
|
self.assertEqual(len(response.feeds), 2)
|
||||||
|
|
||||||
|
# BUG: Group create doesn't work: https://github.com/adafruit/io/issues/58
|
||||||
|
@unittest.expectedFailure
|
||||||
|
def test_create_group(self):
|
||||||
|
io = self.get_client()
|
||||||
|
self.ensure_group_deleted(io, 'GroupTest2')
|
||||||
|
self.ensure_feed_deleted(io, 'GroupTest3')
|
||||||
|
self.ensure_feed_deleted(io, 'GroupTest4')
|
||||||
|
feed1 = io.create_feed(Feed(name='GroupTest3'))
|
||||||
|
feed2 = io.create_feed(Feed(name='GroupTest4'))
|
||||||
|
io.send('GroupTest3', 10)
|
||||||
|
io.send('GroupTest4', 20)
|
||||||
|
group = Group(name='GroupTest2', feeds=[feed1, feed2])
|
||||||
|
response = io.create_group(group)
|
||||||
|
self.assertEqual(response.name, 'GroupTest2')
|
||||||
|
self.assertEqual(len(response.feeds), 2)
|
||||||
|
|
||||||
|
# BUG: Group create doesn't work: https://github.com/adafruit/io/issues/58
|
||||||
|
@unittest.expectedFailure
|
||||||
|
def test_delete_group(self):
|
||||||
|
io = self.get_client()
|
||||||
|
self.ensure_group_deleted(io, 'GroupDeleteTest')
|
||||||
|
group = io.create_group(Group(name='GroupDeleteTest'))
|
||||||
|
io.delete_group('GroupDeleteTest')
|
||||||
|
self.assertRaises(RequestError, io.groups, 'GroupDeleteTest')
|
||||||
|
|
||||||
|
# TODO: Get by group name, key, and ID
|
||||||
|
# TODO: Get data by name, key, ID
|
||||||
|
# TODO: Tests around Adafruit IO keys (make multiple, test they work, etc.)
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
# Test Data instance from REST client.
|
|
||||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
|
||||||
import Adafruit_IO
|
|
||||||
import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestData(base.IOTestCase):
|
|
||||||
|
|
||||||
def test_data_properties_are_optional(self):
|
|
||||||
data = Adafruit_IO.Data(value='foo', feed_id=10)
|
|
||||||
self.assertEqual(data.value, 'foo')
|
|
||||||
self.assertEqual(data.feed_id, 10)
|
|
||||||
self.assertIsNone(data.created_epoch)
|
|
||||||
self.assertIsNone(data.created_at)
|
|
||||||
self.assertIsNone(data.updated_at)
|
|
||||||
self.assertIsNone(data.completed_at)
|
|
||||||
self.assertIsNone(data.expiration)
|
|
||||||
self.assertIsNone(data.position)
|
|
||||||
self.assertIsNone(data.id)
|
|
||||||
|
|
||||||
def test_from_response_ignores_unknown_items(self):
|
|
||||||
data = Adafruit_IO.Data.from_response({'value': 'foo', 'feed_id': 10, 'unknown_param': 42})
|
|
||||||
self.assertEqual(data.value, 'foo')
|
|
||||||
self.assertEqual(data.feed_id, 10)
|
|
||||||
self.assertIsNone(data.created_epoch)
|
|
||||||
self.assertIsNone(data.created_at)
|
|
||||||
self.assertIsNone(data.updated_at)
|
|
||||||
self.assertIsNone(data.completed_at)
|
|
||||||
self.assertIsNone(data.expiration)
|
|
||||||
self.assertIsNone(data.position)
|
|
||||||
self.assertIsNone(data.id)
|
|
||||||
|
|
@ -1,23 +1,41 @@
|
||||||
# Test error responses with REST client.
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
# Author: Tony DiCola
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import Adafruit_IO
|
from Adafruit_IO import Client, RequestError, ThrottlingError
|
||||||
import base
|
import base
|
||||||
|
|
||||||
|
|
||||||
class TestErrors(base.IOTestCase):
|
class TestErrors(base.IOTestCase):
|
||||||
|
|
||||||
def test_request_error_from_bad_key(self):
|
def test_request_error_from_bad_key(self):
|
||||||
io = Adafruit_IO.Client("this is a bad key from a test")
|
io = Client("this is a bad key from a test")
|
||||||
with self.assertRaises(Adafruit_IO.RequestError):
|
with self.assertRaises(RequestError):
|
||||||
io.send("TestStream", 42)
|
io.send("TestStream", 42)
|
||||||
|
|
||||||
@unittest.skip("Throttling test must be run in isolation to prevent other failures.")
|
@unittest.skip("Throttling test must be run in isolation to prevent other tests from failing.")
|
||||||
def test_throttling_error_after_6_requests_in_short_period(self):
|
def test_throttling_error_after_6_requests_in_short_period(self):
|
||||||
io = Adafruit_IO.Client(self.get_test_key())
|
io = Client(self.get_test_key())
|
||||||
with self.assertRaises(Adafruit_IO.ThrottlingError):
|
with self.assertRaises(ThrottlingError):
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
io.send("TestStream", 42)
|
io.send("TestStream", 42)
|
||||||
time.sleep(0.1) # Small delay to keep from hammering network.
|
time.sleep(0.1) # Small delay to keep from hammering network.
|
||||||
|
|
|
||||||
50
tests/test_model.py
Normal file
50
tests/test_model.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
|
# Author: Tony DiCola
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
from Adafruit_IO import Data
|
||||||
|
|
||||||
|
import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestData(base.IOTestCase):
|
||||||
|
|
||||||
|
def test_data_properties_are_optional(self):
|
||||||
|
data = Data(value='foo', feed_id=10)
|
||||||
|
self.assertEqual(data.value, 'foo')
|
||||||
|
self.assertEqual(data.feed_id, 10)
|
||||||
|
self.assertIsNone(data.created_epoch)
|
||||||
|
self.assertIsNone(data.created_at)
|
||||||
|
self.assertIsNone(data.updated_at)
|
||||||
|
self.assertIsNone(data.completed_at)
|
||||||
|
self.assertIsNone(data.expiration)
|
||||||
|
self.assertIsNone(data.position)
|
||||||
|
self.assertIsNone(data.id)
|
||||||
|
|
||||||
|
def test_from_dict_ignores_unknown_items(self):
|
||||||
|
data = Data.from_dict({'value': 'foo', 'feed_id': 10, 'unknown_param': 42})
|
||||||
|
self.assertEqual(data.value, 'foo')
|
||||||
|
self.assertEqual(data.feed_id, 10)
|
||||||
|
self.assertIsNone(data.created_epoch)
|
||||||
|
self.assertIsNone(data.created_at)
|
||||||
|
self.assertIsNone(data.updated_at)
|
||||||
|
self.assertIsNone(data.completed_at)
|
||||||
|
self.assertIsNone(data.expiration)
|
||||||
|
self.assertIsNone(data.position)
|
||||||
|
self.assertIsNone(data.id)
|
||||||
|
|
@ -1,9 +1,27 @@
|
||||||
# Test MQTT client class.
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
# Author: Tony DiCola
|
||||||
import logging
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import Adafruit_IO
|
from Adafruit_IO import MQTTClient
|
||||||
|
|
||||||
import base
|
import base
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,25 +31,27 @@ TIMEOUT_SEC = 5 # Max amount of time (in seconds) to wait for asyncronous event
|
||||||
|
|
||||||
class TestMQTTClient(base.IOTestCase):
|
class TestMQTTClient(base.IOTestCase):
|
||||||
|
|
||||||
def wait_until_connected(self, client, connect_value=True, timeout_sec=TIMEOUT_SEC):
|
def wait_until_connected(self, client, connect_value=True,
|
||||||
|
timeout_sec=TIMEOUT_SEC):
|
||||||
# Pump the specified client message loop and wait until it's connected,
|
# Pump the specified client message loop and wait until it's connected,
|
||||||
# or the specified timeout has ellapsed. Can specify an explicit
|
# or the specified timeout has ellapsed. Can specify an explicit
|
||||||
# connection state to wait for by setting connect_value (defaults to
|
# connection state to wait for by setting connect_value (defaults to
|
||||||
# waiting until connected, i.e. True).
|
# waiting until connected, i.e. True).
|
||||||
start = time.time()
|
start = time.time()
|
||||||
while client.is_connected() != connect_value and (time.time() - start) < timeout_sec:
|
while client.is_connected() != connect_value and \
|
||||||
|
(time.time() - start) < timeout_sec:
|
||||||
client.loop()
|
client.loop()
|
||||||
time.sleep(0)
|
time.sleep(0)
|
||||||
|
|
||||||
def test_create_client(self):
|
def test_create_client(self):
|
||||||
# Create MQTT test client.
|
# Create MQTT test client.
|
||||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
client = MQTTClient(self.get_test_key())
|
||||||
# Verify not connected by default.
|
# Verify not connected by default.
|
||||||
self.assertFalse(client.is_connected())
|
self.assertFalse(client.is_connected())
|
||||||
|
|
||||||
def test_connect(self):
|
def test_connect(self):
|
||||||
# Create MQTT test client.
|
# Create MQTT test client.
|
||||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
client = MQTTClient(self.get_test_key())
|
||||||
# Verify on_connect handler is called and expected client is provided.
|
# Verify on_connect handler is called and expected client is provided.
|
||||||
def on_connect(mqtt_client):
|
def on_connect(mqtt_client):
|
||||||
self.assertEqual(mqtt_client, client)
|
self.assertEqual(mqtt_client, client)
|
||||||
|
|
@ -44,7 +64,7 @@ class TestMQTTClient(base.IOTestCase):
|
||||||
|
|
||||||
def test_disconnect(self):
|
def test_disconnect(self):
|
||||||
# Create MQTT test client.
|
# Create MQTT test client.
|
||||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
client = MQTTClient(self.get_test_key())
|
||||||
# Verify on_connect handler is called and expected client is provided.
|
# Verify on_connect handler is called and expected client is provided.
|
||||||
def on_disconnect(mqtt_client):
|
def on_disconnect(mqtt_client):
|
||||||
self.assertEqual(mqtt_client, client)
|
self.assertEqual(mqtt_client, client)
|
||||||
|
|
@ -60,12 +80,12 @@ class TestMQTTClient(base.IOTestCase):
|
||||||
|
|
||||||
def test_subscribe_and_publish(self):
|
def test_subscribe_and_publish(self):
|
||||||
# Create MQTT test client.
|
# Create MQTT test client.
|
||||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
client = MQTTClient(self.get_test_key())
|
||||||
# Save all on_message handler responses.
|
# Save all on_message handler responses.
|
||||||
messages = []
|
messages = []
|
||||||
def on_message(mqtt_client, feed_id, payload):
|
def on_message(mqtt_client, feed, payload):
|
||||||
self.assertEqual(mqtt_client, client)
|
self.assertEqual(mqtt_client, client)
|
||||||
messages.append((feed_id, payload))
|
messages.append((feed, payload))
|
||||||
client.on_message = on_message
|
client.on_message = on_message
|
||||||
# Connect and wait until on_connect event is fired.
|
# Connect and wait until on_connect event is fired.
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue