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:
Tony DiCola 2015-02-01 12:22:03 -08:00
parent 2ee81791a2
commit 9900d508f5
19 changed files with 1354 additions and 361 deletions

View file

@ -1,2 +1,24 @@
from .client import Client, AdafruitIOError, RequestError, ThrottlingError, Data
from .mqtt_client import MQTTClient
# 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 .client import Client
from .mqtt_client import MQTTClient
from .errors import AdafruitIOError, RequestError, ThrottlingError
from .model import Data, Stream, Feed, Group

View file

@ -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
from urllib3 import connection_from_url
from urllib import urlencode, quote
import requests
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):
"""Client instance for interacting with the Adafruit IO service using its 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.
"""Client instance for interacting with the Adafruit IO service using its
REST API. Use this client class to send, receive, and enumerate feed data.
"""
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):
return connection_from_url(url)
def __init__(self, key, proxies=None, base_url='https://io.adafruit.com'):
"""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):
return self.BASE_URL + path
def _compose_url(self, path):
return '{0}/{1}'.format(self.base_url, path)
def _compose_get_url(self, path, params=None):
return self.BASE_URL + path + '?' + urlencode(params)
def _handle_error(sefl, response):
# 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):
# Handle explicit errors.
if response.status == 429:
raise ThrottlingError()
# Handle all other errors (400 & 500 level HTTP responses)
elif response.status >= 400:
raise RequestError(response)
# Else do nothing if there was no error.
def _get(self, path):
response = requests.get(self._compose_url(path),
headers={'X-AIO-Key': self.key},
proxies=self.proxies)
self._handle_error(response)
return response.json()
def _handle_response(self, response, expect_result):
self._handle_error(response)
if expect_result:
return json.loads(response.data)
# Else no result expected so just return.
def _post(self, path, data):
response = requests.post(self._compose_url(path),
headers={'X-AIO-Key': self.key,
'Content-Type': 'application/json'},
proxies=self.proxies,
data=json.dumps(data))
self._handle_error(response)
return response.json()
def _request(self, method, path, params=None, expect_result=True):
if (method.lower() == "get"):
url = self._compose_get_url(path, params)
else:
url = self._compose_url(path)
def _delete(self, path):
response = requests.delete(self._compose_url(path),
headers={'X-AIO-Key': self.key,
'Content-Type': 'application/json'},
proxies=self.proxies)
self._handle_error(response)
self.rate_limit_lock and self.rate_limit_lock.acquire()
headers = {"X-AIO-Key": self.key, 'Content-Type':'application/json'}
if (method.upper() == "GET"):
r = self.connection_pool.urlopen(method.upper(), url, headers=headers)
else:
r = self.connection_pool.urlopen(method.upper(), url, headers=headers,
body=json.dumps(params))
# Data functionality.
def send(self, feed_name, value):
"""Helper function to simplify adding a value to a feed. Will find the
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 about the newly appended row of data.
"""
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):
return self._request('GET', path, params=params)
def receive_next(self, feed):
"""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):
return self._request('POST', path, params=params)
def receive_previous(self, feed):
"""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):
return self._request('DELETE', path, expect_result=False)
def data(self, feed, data_id=None):
"""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 delete_feed(self, feed):
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed name.
"""
feed = quote(feed)
path = "api/feeds/{}".format(feed)
self._delete(path)
def create_data(self, feed, data):
"""Create a new row of data in the specified feed. Feed can be a feed
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
details about the newly appended row of data.
"""
path = "api/feeds/{0}/data".format(feed)
return Data.from_dict(self._post(path, data._asdict()))
#feed data functionality
def send(self, feed_name, value):
"""Helper function to simplify adding a value to a feed. Will find the
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
about the newly appended row of data.
"""
feed_name = quote(feed_name)
path = "api/feeds/{}/data/send".format(feed_name)
return Data.from_response(self._post(path, {'value': value}))
def delete(self, feed, data_id):
"""Delete data from a feed. Feed can be a feed ID, feed key, or feed
name. Data_id must be the ID of the piece of data to delete.
"""
path = "api/feeds/{0}/data/{1}".format(feed, data_id)
self._delete(path)
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.
"""
feed = quote(feed)
path = "api/feeds/{}/data/last".format(feed)
return Data.from_response(self._get(path))
# Feed functionality.
def feeds(self, feed=None):
"""Retrieve a list of all feeds, or the specified feed. If feed is not
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.
"""
if feed is None:
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):
"""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.
"""
feed = quote(feed)
path = "api/feeds/{}/data/next".format(feed)
return Data.from_response(self._get(path))
def create_feed(self, feed):
"""Create the specified feed. Feed should be an instance of the Feed
type with at least the name property set.
"""
path = "api/feeds/"
return Feed.from_dict(self._post(path, feed._asdict()))
def receive_previous(self, feed):
"""Retrieve the previously read 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.
"""
feed = quote(feed)
path = "api/feeds/{}/data/last".format(feed)
return Data.from_response(self._get(path))
def delete_feed(self, feed):
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed
name.
"""
path = "api/feeds/{0}".format(feed)
self._delete(path)
def data(self, feed, data_id=None):
"""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/{}/data".format(feed)
return map(Data.from_response, self._get(path))
else:
path = "api/feeds/{}/data/{}".format(feed, data_id)
return Data.from_response(self._get(path))
# Group functionality.
def send_group(self, group_name, data):
"""Update all feeds in a group with one call. Group_name should be the
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
data row value. For example a group 'TestGroup' with feeds 'FeedOne'
and 'FeedTwo' could be updated by calling:
send_group('TestGroup', {'FeedOne': 'value1', 'FeedTwo': 10})
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):
"""Create a new row of data in the specified feed. Feed can be a feed 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 details
about the newly appended row of data.
"""
path = "api/feeds/{}/data".format(feed)
return Data.from_response(self._post(path, data._asdict()))
After a successful update an instance of Group will be returned with
metadata about the updated group.
"""
path = "api/groups/{0}/send".format(group_name)
return Group.from_dict(self._post(path, {'value': data}))
#group functionality
def send_group(self, group_name, data):
group_name = quote(group_name)
path = "api/groups/{}/send".format(group_name)
return self._post(path, {'value': data})
def receive_group(self, group):
"""Retrieve the most recent value for the specified group. Group can be
a group ID, group key, or group name. Returns a Group instance whose
feeds property holds an array of Feed instances associated with the group.
"""
path = "api/groups/{0}/last".format(group)
return Group.from_dict(self._get(path))
def receive_group(self, group_name):
group_name = quote(group_name)
path = "api/groups/{}/last".format(group_name)
return self._get(path)
def receive_next_group(self, group):
"""Retrieve the next unread value from the specified group. Group can
be a group ID, group key, or group name. Returns a Group instance whose
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):
group_name = quote(group_name)
path = "api/groups/{}/next".format(group_name)
return self._get(path)
def receive_previous_group(self, group):
"""Retrieve the previous unread value from the specified group. Group
can be a group ID, group key, or group name. Returns a Group instance
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):
group_name = quote(group_name)
path = "api/groups/{}/last".format(group_name)
return self._get(path)
def groups(self, group=None):
"""Retrieve a list of all groups, or the specified group. If group is
not specified a list of all groups will be returned. If group is
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):
path = "api/groups/{}".format(group_id_or_key)
return self._get(path)
def create_group(self, group):
"""Create the specified group. Group should be an instance of the Group
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):
path = "api/groups/{}".format(group_id_or_key)
return self._post(path, data)
def delete_group(self, group):
"""Delete the specified group. Group can be a group ID, group key, or
group name.
"""
path = "api/groups/{0}".format(group)
self._delete(path)

40
Adafruit_IO/errors.py Normal file
View 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
View 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)

View file

@ -1,18 +1,29 @@
# MQTT-based client for Adafruit.IO
# Author: Tony DiCola (tdicola@adafruit.com)
#
# Supports publishing and subscribing to feed changes from Adafruit IO using
# the MQTT protcol.
#
# Depends on the following Python libraries:
# - paho-mqtt: Paho MQTT client for python.
# 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.
import logging
import paho.mqtt.client as mqtt
SERVICE_HOST = 'io.adafruit.com'
SERVICE_PORT = 1883
# How long to wait before sending a keep alive (paho-mqtt configuration).
KEEP_ALIVE_SEC = 3600 # One minute
logger = logging.getLogger(__name__)
@ -23,12 +34,14 @@ class MQTTClient(object):
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.
Required parameters:
- 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.
self.on_connect = None
self.on_disconnect = None
@ -69,11 +82,12 @@ class MQTTClient(object):
def _mqtt_message(self, client, userdata, msg):
logger.debug('Client on_message called.')
# 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/') \
and len(msg.topic) >= 28:
feed_id = msg.topic[10:-18]
self.on_message(self, feed_id, msg.payload)
feed = msg.topic[10:-18]
payload = '' if msg.payload is None else msg.payload.decode('utf-8')
self.on_message(self, feed, payload)
def connect(self, **kwargs):
"""Connect to the Adafruit.IO service. Must be called before any loop
@ -85,7 +99,7 @@ class MQTTClient(object):
if self._connected:
return
# 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)
def is_connected(self):
@ -94,7 +108,7 @@ class MQTTClient(object):
return self._connected
def disconnect(self):
# Disconnect MQTT client if connected.
"""Disconnect MQTT client if connected."""
if self._connected:
self._client.disconnect()

View file

@ -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
----
Initial Changelog

View file

@ -1,5 +1,5 @@
Copyright (c) 2014 Adafruit
Author: Justin Cooper
Author: Justin Cooper and Tony DiCola
MIT License

287
README.md
View file

@ -1,11 +1,14 @@
# 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
### 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)
(typically with ````apt-get install python-pip```` on a Debian/Ubuntu-based
system) then run:
@ -31,25 +34,24 @@ At a high level the Adafruit IO python client provides two interfaces to the
service:
* 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.
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
all times.
quickly as possible.
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:
```python
import Adafruit_IO
from Adafruit_IO import *
```
Then a REST API client can be created with code like:
```python
aio = Adafruit_IO.Client('xxxxxxxxxxxx')
aio = Client('xxxxxxxxxxxx')
```
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:
```python
mqtt = Adafruit_IO.MQTTClient('xxxxxxxxxxxx')
mqtt = MQTTClient('xxxxxxxxxxxx')
```
Again where 'xxxxxxxxxxxx' is your Adafruit IO API key.
@ -83,28 +85,44 @@ example uses the REST API.
from Adafruit_IO import Client
aio = Client('YOUR ADAFRUIT IO KEY')
# Send a value to a feed called 'Feed Name'.
# Data can be of any type, string, number, hash, json.
aio.send('Feed Name', data)
# Send the value 100 to a feed called 'Foo'.
aio.send('Foo', 100)
# Retrieve the most recent value from the feed 'Feed Name'.
# Notice the returned object has a property called value.
data = aio.receive("Feed Name")
# Retrieve the most recent value from the feed 'Foo'.
# Access the value by reading the `value` property on the returned Data object.
# 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))
```
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.
### 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
* [Feeds](#feeds)
* [Creation, Retrieval, Updating](#feed-creation-retrieval-updating)
* [Create](#feed-creation)
* [Read](#feed-retrieval)
* [Update](#feed-updating)
* [Delete](#feed-deletion)
* [Data](#data)
* [Create](#data-creation)
* [Read](#data-retrieval)
* [Updating, Deletion](#data-updating-deletion)
* [Update](#data-updating)
* [Delete](#data-deletion)
* [Helper Methods](#helper-methods)
* [Send](#send)
* [Receive](#receive)
@ -112,6 +130,10 @@ using the MQTT client. See the [examples\mqtt_client.py](https://github.com/ada
* [Previous](#previous)
* [Publishing and Subscribing](#publishing-and-subscribing)
* [Groups](#groups)
* [Create](#group-creation)
* [Read](#group-retrieval)
* [Update](#group-updating)
* [Delete](#group-deletion)
### 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
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
updating a feed's metadata. See the send helper function below for creating a
feed and sending it new data.
Create a feed by constructing a Feed instance with at least a name specified, and
then pass it to the `create_feed(feed)` function:
```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
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!
```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'.
aio.delete_feed('Test')
```
@ -145,47 +225,76 @@ and selecting certain pieces of data.
#### Data Creation
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
a value set. See the [send function](#send) for a simpler and recommended way
of adding a new value to a feed.
`create_data(feed, data)` method and passing it a new Data instance a value.
See also the [send function](#send) for a simpler way to add a value to feed and
create the feed in one call.
```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.
data = Adafruit_IO.Data(value=10)
aio.create_stream('Test', data)
data = Data(value=10)
aio.create_data('Test', data)
```
#### 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
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
# 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'
data = aio.streams('Test')
data = aio.data('Test')
# Print out all the results.
for d in data:
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
if it exists in the feed. The returned object will be an instance of the Data
class.
```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.
# This example assumes 1 is a valid data ID in the 'Test' feed
data = aio.feeds('Test', 1)
# Print the 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
data.
TODO: This is not tested in the python client, but calling create_data with a
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
@ -193,46 +302,72 @@ There are a few helper methods that can make interacting with data a bit easier.
##### 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
created. This is the recommended way to send data to Adafruit IO from the Python
client.
REST client.
```python
# Add the value 98.6 to the feed 'Test Send Data'.
aio.send('Test Send Data', 98.6)
# Import library and create instance of REST client.
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
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
# 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')
# 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
You can get the first inserted value that has not been processed by using the
`aio.receive_next(feed)` method.
You can get the first inserted value that has not been processed (read) by using
the `receive_next(feed)` method.
```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')
# Print the value.
print('Data value: {0}'.format(data))
print('Data value: {0}'.format(data.value))
```
##### Previous
You can get the the last record that has been processed by using the
`aio.receive_previous(feed)` method.
You can get the the last record that has been processed (read) by using the
`receive_previous(feed)` method.
```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')
# Print the value.
print('Data value: {0}'.format(data))
print('Data value: {0}'.format(data.value))
```
#### 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
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 allow you to update and retrieve multiple feeds with one request. You can
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

View file

@ -1,13 +1,13 @@
# 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 random
import sys
import time
# Import Adafruit IO client.
import Adafruit_IO
# Import Adafruit IO MQTT client.
from Adafruit_IO import MQTTClient
# Set to your Adafruit IO key.
@ -37,7 +37,7 @@ def message(client, feed_id, payload):
# Create an MQTT client instance.
client = Adafruit_IO.MQTTClient(ADAFRUIT_IO_KEY)
client = MQTTClient(ADAFRUIT_IO_KEY)
# Setup the callback functions defined above.
client.on_connect = connected

32
examples/simple.py Normal file
View 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))

View file

@ -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
View 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())

View file

@ -1,8 +1,10 @@
from ez_setup import use_setuptools
use_setuptools()
from setuptools import setup
setup(
name='adafruit-io',
version='0.0.1',
version='0.9.0',
author='Justin Cooper',
author_email='justin@adafruit.com',
packages=['Adafruit_IO'],
@ -11,6 +13,7 @@ setup(
description='IO Client library for io.adafruit.com',
long_description=open('README.md').read(),
install_requires=[
"apiclient >= 1.0.2"
"requests",
"paho-mqtt"
],
)

View file

@ -1,10 +1,25 @@
# Base testcase class with functions and state available to all tests.
# Author: Tony DiCola (tdicola@adafruit.com)
import os
import time
import unittest
# Copyright (c) 2014 Adafruit Industries
# Author: Tony DiCola
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):
@ -18,13 +33,3 @@ class IOTestCase(unittest.TestCase):
raise RuntimeError("ADAFRUIT_IO_KEY environment variable must be " \
"set with valid Adafruit IO key to run this test!")
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

View file

@ -1,13 +1,22 @@
# Test REST client.
# Author: Tony DiCola (tdicola@adafruit.com)
import time
import unittest
from Adafruit_IO import Client, Data, Feed, Group, RequestError
import Adafruit_IO
import base
# Turn this on to see responses of requests from urllib.
#import logging
#logging.basicConfig(level=logging.DEBUG)
# Default config for tests to run against real Adafruit IO service with no proxy.
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):
@ -16,24 +25,39 @@ class TestClient(base.IOTestCase):
#def tearDown(self):
# 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):
key = "unique_key_id"
io = Adafruit_IO.Client(key)
io = Client(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):
io = Adafruit_IO.Client(self.get_test_key())
io = self.get_client()
self.ensure_feed_deleted(io, 'TestFeed')
response = io.send('TestFeed', 'foo')
self.assertEqual(response.value, 'foo')
@ -41,22 +65,28 @@ class TestClient(base.IOTestCase):
self.assertEqual(data.value, 'foo')
def test_receive_next(self):
io = Adafruit_IO.Client(self.get_test_key())
io = self.get_client()
self.ensure_feed_deleted(io, 'TestFeed')
io.send('TestFeed', 1)
data = io.receive_next('TestFeed')
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):
io = Adafruit_IO.Client(self.get_test_key())
io = self.get_client()
self.ensure_feed_deleted(io, 'TestFeed')
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')
self.assertEqual(int(data.value), 1)
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')
io.send('TestFeed', 1)
io.send('TestFeed', 2)
@ -66,7 +96,7 @@ class TestClient(base.IOTestCase):
self.assertEqual(int(result[1].value), 2)
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')
data = io.send('TestFeed', 1)
result = io.data('TestFeed', data.id)
@ -74,9 +104,135 @@ class TestClient(base.IOTestCase):
self.assertEqual(int(data.value), int(result.value))
def test_create_data(self):
io = Adafruit_IO.Client(self.get_test_key())
io = self.get_client()
self.ensure_feed_deleted(io, 'TestFeed')
io.send('TestFeed', 1) # Make sure TestFeed exists.
data = Adafruit_IO.Data(value=42)
data = Data(value=42)
result = io.create_data('TestFeed', data)
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.)

View file

@ -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)

View file

@ -1,23 +1,41 @@
# Test error responses with REST client.
# Author: Tony DiCola (tdicola@adafruit.com)
# 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.
import time
import unittest
import Adafruit_IO
from Adafruit_IO import Client, RequestError, ThrottlingError
import base
class TestErrors(base.IOTestCase):
def test_request_error_from_bad_key(self):
io = Adafruit_IO.Client("this is a bad key from a test")
with self.assertRaises(Adafruit_IO.RequestError):
io = Client("this is a bad key from a test")
with self.assertRaises(RequestError):
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):
io = Adafruit_IO.Client(self.get_test_key())
with self.assertRaises(Adafruit_IO.ThrottlingError):
io = Client(self.get_test_key())
with self.assertRaises(ThrottlingError):
for i in range(6):
io.send("TestStream", 42)
time.sleep(0.1) # Small delay to keep from hammering network.

50
tests/test_model.py Normal file
View 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)

View file

@ -1,9 +1,27 @@
# Test MQTT client class.
# Author: Tony DiCola (tdicola@adafruit.com)
import logging
# 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.
import time
import Adafruit_IO
from Adafruit_IO import MQTTClient
import base
@ -13,25 +31,27 @@ TIMEOUT_SEC = 5 # Max amount of time (in seconds) to wait for asyncronous event
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,
# or the specified timeout has ellapsed. Can specify an explicit
# connection state to wait for by setting connect_value (defaults to
# waiting until connected, i.e. True).
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()
time.sleep(0)
def test_create_client(self):
# Create MQTT test client.
client = Adafruit_IO.MQTTClient(self.get_test_key())
client = MQTTClient(self.get_test_key())
# Verify not connected by default.
self.assertFalse(client.is_connected())
def test_connect(self):
# 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.
def on_connect(mqtt_client):
self.assertEqual(mqtt_client, client)
@ -44,7 +64,7 @@ class TestMQTTClient(base.IOTestCase):
def test_disconnect(self):
# 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.
def on_disconnect(mqtt_client):
self.assertEqual(mqtt_client, client)
@ -60,12 +80,12 @@ class TestMQTTClient(base.IOTestCase):
def test_subscribe_and_publish(self):
# Create MQTT test client.
client = Adafruit_IO.MQTTClient(self.get_test_key())
client = MQTTClient(self.get_test_key())
# Save all on_message handler responses.
messages = []
def on_message(mqtt_client, feed_id, payload):
def on_message(mqtt_client, feed, payload):
self.assertEqual(mqtt_client, client)
messages.append((feed_id, payload))
messages.append((feed, payload))
client.on_message = on_message
# Connect and wait until on_connect event is fired.
client.connect()