diff --git a/.gitignore b/.gitignore index 4960f7d..865f577 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.pyc +.tox +.coverage withings.conf +withings.egg-info diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6d3729f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: 3.3 +env: + - TOX_ENV=pypy + - TOX_ENV=py27 + - TOX_ENV=py26 +install: + - pip install coveralls tox +script: tox -e $TOX_ENV +after_success: coveralls diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..37a3c7a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE README.md requirements/* diff --git a/requirements.txt b/requirements/base.txt similarity index 100% rename from requirements.txt rename to requirements/base.txt diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..408831f --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,5 @@ +-r base.txt + +coverage==3.7.1 +mock==1.0.1 +tox==1.7.2 diff --git a/setup.py b/setup.py index 7148a71..3c94f21 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,8 @@ #!/usr/bin/env python from setuptools import setup +required = [line for line in open('requirements/base.txt').read().split("\n")] + setup( name='withings', version='0.3', @@ -10,7 +12,8 @@ url="https://github.com/maximebf/python-withings", license = "MIT License", packages = ['withings'], - install_requires = ['requests', 'requests-oauth'], + install_requires = required, + test_suite='tests.all_tests', scripts=['bin/withings'], keywords="withings", zip_safe = True diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..ce11970 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,17 @@ +import unittest + +from .test_withings_credentials import TestWithingsCredentials +from .test_withings_auth import TestWithingsAuth +from .test_withings_api import TestWithingsApi +from .test_withings_measures import TestWithingsMeasures +from .test_withings_measure_group import TestWithingsMeasureGroup + + +def all_tests(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestWithingsCredentials)) + suite.addTest(unittest.makeSuite(TestWithingsAuth)) + suite.addTest(unittest.makeSuite(TestWithingsApi)) + suite.addTest(unittest.makeSuite(TestWithingsMeasures)) + suite.addTest(unittest.makeSuite(TestWithingsMeasureGroup)) + return suite diff --git a/tests/test_withings_api.py b/tests/test_withings_api.py new file mode 100644 index 0000000..81f711f --- /dev/null +++ b/tests/test_withings_api.py @@ -0,0 +1,232 @@ +import json +import time +import unittest + +from datetime import datetime +from requests import Session +from withings import ( + WithingsApi, + WithingsCredentials, + WithingsMeasureGroup, + WithingsMeasures +) + +try: + import configparser +except ImportError: # Python 2.x fallback + import ConfigParser as configparser + +try: + from unittest.mock import MagicMock +except ImportError: # Python 2.x fallback + from mock import MagicMock + + +class TestWithingsApi(unittest.TestCase): + def setUp(self): + self.mock_api = True + if self.mock_api: + self.creds = WithingsCredentials() + else: + config = ConfigParser.ConfigParser() + config.read('withings.conf') + self.creds = WithingsCredentials( + consumer_key=config.get('withings', 'consumer_key'), + consumer_secret=config.get('withings', 'consumer_secret'), + access_token=config.get('withings', 'access_token'), + access_token_secret=config.get('withings', + 'access_token_secret'), + user_id=config.get('withings', 'user_id')) + self.api = WithingsApi(self.creds) + + def test_attributes(self): + """ Make sure the WithingsApi objects have the right attributes """ + assert hasattr(WithingsApi, 'URL') + creds = WithingsCredentials(user_id='FAKEID') + api = WithingsApi(creds) + assert hasattr(api, 'credentials') + assert hasattr(api, 'oauth') + assert hasattr(api, 'client') + + def test_attribute_defaults(self): + """ + Make sure WithingsApi object attributes have the correct defaults + """ + self.assertEqual(WithingsApi.URL, 'http://wbsapi.withings.net') + creds = WithingsCredentials(user_id='FAKEID') + api = WithingsApi(creds) + self.assertEqual(api.credentials, creds) + self.assertEqual(api.client.auth, api.oauth) + self.assertEqual(api.client.params, {'userid': creds.user_id}) + + def test_request(self): + """ + Make sure the request method builds the proper URI and returns the + request body as a python dict. + """ + self.mock_request({}) + resp = self.api.request('fake_service', 'fake_action') + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/fake_service', + params={'action': 'fake_action'}) + self.assertEqual(resp, {}) + + def test_request_params(self): + """ + Check that the request method passes along extra params and works + with different HTTP methods + """ + self.mock_request({}) + resp = self.api.request('user', 'getbyuserid', params={'p2': 'p2'}, + method='POST') + Session.request.assert_called_once_with( + 'POST', 'http://wbsapi.withings.net/user', + params={'p2': 'p2', 'action': 'getbyuserid'}) + self.assertEqual(resp, {}) + + def test_request_error(self): + """ Check that requests raises an exception when there is an error """ + self.mock_request('', status=1) + self.assertRaises(Exception, self.api.request, ('user', 'getbyuserid')) + + def test_get_user(self): + """ Check that the get_user method fetches the right URL """ + self.mock_request({ + 'users': [ + {'id': 1111111, 'birthdate': 364305600, 'lastname': 'Baggins', + 'ispublic': 255, 'firstname': 'Frodo', 'fatmethod': 131, + 'gender': 0, 'shortname': 'FRO'} + ] + }) + resp = self.api.get_user() + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/user', + params={'action': 'getbyuserid'}) + self.assertEqual(type(resp), dict) + assert 'users' in resp + self.assertEqual(type(resp['users']), list) + self.assertEqual(len(resp['users']), 1) + self.assertEqual(resp['users'][0]['firstname'], 'Frodo') + self.assertEqual(resp['users'][0]['lastname'], 'Baggins') + + def test_get_measures(self): + """ + Check that get_measures fetches the appriate URL, the response looks + correct, and the return value is a WithingsMeasures object + """ + body = { + 'updatetime': 1409596058, + 'measuregrps': [ + {'attrib': 2, 'measures': [ + {'unit': -1, 'type': 1, 'value': 860} + ], 'date': 1409361740, 'category': 1, 'grpid': 111111111}, + {'attrib': 2, 'measures': [ + {'unit': -2, 'type': 4, 'value': 185} + ], 'date': 1409361740, 'category': 1, 'grpid': 111111112} + ] + } + self.mock_request(body) + resp = self.api.get_measures() + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/measure', + params={'action': 'getmeas'}) + self.assertEqual(type(resp), WithingsMeasures) + self.assertEqual(len(resp), 2) + self.assertEqual(type(resp[0]), WithingsMeasureGroup) + self.assertEqual(resp[0].weight, 86.0) + self.assertEqual(resp[1].height, 1.85) + + # Test limit=1 + body['measuregrps'].pop() + self.mock_request(body) + resp = self.api.get_measures(limit=1) + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/measure', + params={'action': 'getmeas', 'limit': 1}) + self.assertEqual(len(resp), 1) + self.assertEqual(resp[0].weight, 86.0) + + def test_subscribe(self): + """ + Check that subscribe fetches the right URL and returns the expected + results + """ + self.mock_request(None) + resp = self.api.subscribe('http://www.example.com/', 'fake_comment') + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/notify', + params={'action': 'subscribe', 'appli': 1, + 'comment': 'fake_comment', + 'callbackurl': 'http://www.example.com/'}) + self.assertEqual(resp, None) + + def test_unsubscribe(self): + """ + Check that unsubscribe fetches the right URL and returns the expected + results + """ + self.mock_request(None) + resp = self.api.unsubscribe('http://www.example.com/') + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/notify', + params={'action': 'revoke', 'appli': 1, + 'callbackurl': 'http://www.example.com/'}) + self.assertEqual(resp, None) + + + def test_is_subscribed(self): + """ + Check that is_subscribed fetches the right URL and returns the + expected results + """ + url = 'http://wbsapi.withings.net/notify' + params = { + 'callbackurl': 'http://www.example.com/', + 'action': 'get', + 'appli': 1 + } + self.mock_request({'expires': 2147483647, 'comment': 'fake_comment'}) + resp = self.api.is_subscribed('http://www.example.com/') + Session.request.assert_called_once_with('GET', url, params=params) + self.assertEquals(resp, True) + + # Not subscribed + self.mock_request(None, status=343) + resp = self.api.is_subscribed('http://www.example.com/') + Session.request.assert_called_once_with('GET', url, params=params) + self.assertEquals(resp, False) + + def test_list_subscriptions(self): + """ + Check that list_subscriptions fetches the right URL and returns the + expected results + """ + self.mock_request({'profiles': [ + {'comment': 'fake_comment', 'expires': 2147483647} + ]}) + resp = self.api.list_subscriptions() + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/notify', + params={'action': 'list', 'appli': 1}) + self.assertEqual(type(resp), list) + self.assertEqual(len(resp), 1) + self.assertEqual(resp[0]['comment'], 'fake_comment') + self.assertEqual(resp[0]['expires'], 2147483647) + + # No subscriptions + self.mock_request({'profiles': []}) + resp = self.api.list_subscriptions() + Session.request.assert_called_once_with( + 'GET', 'http://wbsapi.withings.net/notify', + params={'action': 'list', 'appli': 1}) + self.assertEqual(type(resp), list) + self.assertEqual(len(resp), 0) + + def mock_request(self, body, status=0): + if self.mock_api: + json_content = {'status': status} + if body != None: + json_content['body'] = body + response = MagicMock() + response.content = json.dumps(json_content).encode('utf8') + Session.request = MagicMock(return_value=response) diff --git a/tests/test_withings_auth.py b/tests/test_withings_auth.py new file mode 100644 index 0000000..ed47f89 --- /dev/null +++ b/tests/test_withings_auth.py @@ -0,0 +1,67 @@ +import unittest + +from withings import WithingsAuth, WithingsCredentials +from requests_oauthlib import OAuth1Session + +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock + + +class TestWithingsAuth(unittest.TestCase): + def setUp(self): + self.consumer_key = 'fake_consumer_key' + self.consumer_secret = 'fake_consumer_secret' + self.request_token = { + 'oauth_token': 'fake_oauth_token', + 'oauth_token_secret': 'fake_oauth_token_secret' + } + self.access_token = self.request_token + self.access_token.update({'userid': 'FAKEID'}) + OAuth1Session.fetch_request_token = MagicMock( + return_value=self.request_token) + OAuth1Session.authorization_url = MagicMock(return_value='URL') + OAuth1Session.fetch_access_token = MagicMock( + return_value=self.access_token) + + def test_attributes(self): + """ Make sure the WithingsAuth objects have the right attributes """ + assert hasattr(WithingsAuth, 'URL') + auth = WithingsAuth(self.consumer_key, self.consumer_secret) + assert hasattr(auth, 'consumer_key') + self.assertEqual(auth.consumer_key, self.consumer_key) + assert hasattr(auth, 'consumer_secret') + self.assertEqual(auth.consumer_secret, self.consumer_secret) + + def test_attribute_defaults(self): + """ Make sure WithingsAuth attributes have the proper defaults """ + self.assertEqual(WithingsAuth.URL, + 'https://oauth.withings.com/account') + auth = WithingsAuth(self.consumer_key, self.consumer_secret) + self.assertEqual(auth.oauth_token, None) + self.assertEqual(auth.oauth_secret, None) + + def test_get_authorize_url(self): + """ Make sure the get_authorize_url function works as expected """ + auth = WithingsAuth(self.consumer_key, self.consumer_secret) + # Returns the OAuth1Session.authorization_url results + self.assertEqual(auth.get_authorize_url(), 'URL') + # oauth_token and oauth_secret have now been set to the values + # returned by OAuth1Session.fetch_request_token + self.assertEqual(auth.oauth_token, 'fake_oauth_token') + self.assertEqual(auth.oauth_secret, 'fake_oauth_token_secret') + + def test_get_credentials(self): + """ Make sure the get_credentials function works as expected """ + auth = WithingsAuth(self.consumer_key, self.consumer_secret) + # Returns an authorized WithingsCredentials object + creds = auth.get_credentials('FAKE_OAUTH_VERIFIER') + assert isinstance(creds, WithingsCredentials) + # Check that the attributes of the WithingsCredentials object are + # correct. + self.assertEqual(creds.access_token, 'fake_oauth_token') + self.assertEqual(creds.access_token_secret, 'fake_oauth_token_secret') + self.assertEqual(creds.consumer_key, self.consumer_key) + self.assertEqual(creds.consumer_secret, self.consumer_secret) + self.assertEqual(creds.user_id, 'FAKEID') diff --git a/tests/test_withings_credentials.py b/tests/test_withings_credentials.py new file mode 100644 index 0000000..e1ae312 --- /dev/null +++ b/tests/test_withings_credentials.py @@ -0,0 +1,35 @@ +import unittest + +from withings import WithingsAuth, WithingsCredentials + + +class TestWithingsCredentials(unittest.TestCase): + + def test_attributes(self): + """ + Make sure the WithingsCredentials objects have the right attributes + """ + creds = WithingsCredentials(access_token=1, access_token_secret=1, + consumer_key=1, consumer_secret=1, + user_id=1) + assert hasattr(creds, 'access_token') + self.assertEqual(creds.access_token, 1) + assert hasattr(creds, 'access_token_secret') + self.assertEqual(creds.access_token_secret, 1) + assert hasattr(creds, 'consumer_key') + self.assertEqual(creds.consumer_key, 1) + assert hasattr(creds, 'consumer_secret') + self.assertEqual(creds.consumer_secret, 1) + assert hasattr(creds, 'user_id') + self.assertEqual(creds.user_id, 1) + + def test_attribute_defaults(self): + """ + Make sure WithingsCredentials attributes have the proper defaults + """ + creds = WithingsCredentials() + self.assertEqual(creds.access_token, None) + self.assertEqual(creds.access_token_secret, None) + self.assertEqual(creds.consumer_key, None) + self.assertEqual(creds.consumer_secret, None) + self.assertEqual(creds.user_id, None) diff --git a/tests/test_withings_measure_group.py b/tests/test_withings_measure_group.py new file mode 100644 index 0000000..f1ed806 --- /dev/null +++ b/tests/test_withings_measure_group.py @@ -0,0 +1,129 @@ +import time +import unittest + +from withings import WithingsMeasureGroup + + +class TestWithingsMeasureGroup(unittest.TestCase): + def test_attributes(self): + """ + Check that attributes get set as expected when creating a + WithingsMeasureGroup object + """ + data = { + 'attrib': 2, + 'measures': [ + {'unit': -1, 'type': 1, 'value': 860} + ], + 'date': 1409361740, + 'category': 1, + 'grpid': 111111111 + } + group = WithingsMeasureGroup(data) + self.assertEqual(group.data, data) + self.assertEqual(group.grpid, data['grpid']) + self.assertEqual(group.attrib, data['attrib']) + self.assertEqual(group.category, data['category']) + self.assertEqual(time.mktime(group.date.timetuple()), 1409361740) + self.assertEqual(group.measures, data['measures']) + for _type, type_id in WithingsMeasureGroup.MEASURE_TYPES: + assert hasattr(group, _type) + self.assertEqual(getattr(group, _type), + 86.0 if _type == 'weight' else None) + + def test_types(self): + """ + Check that all the different measure types are working as expected + """ + for _, type_id in WithingsMeasureGroup.MEASURE_TYPES: + data = { + 'attrib': 2, + 'measures': [ + {'unit': -1, 'type': type_id, 'value': 860} + ], + 'date': 1409361740, + 'category': 1, + 'grpid': 111111111 + } + group = WithingsMeasureGroup(data) + for _type, type_id2 in WithingsMeasureGroup.MEASURE_TYPES: + assert hasattr(group, _type) + self.assertEqual(getattr(group, _type), + 86.0 if type_id == type_id2 else None) + + def test_multigroup_types(self): + """ + Check that measure typse with multiple measurements in the group are + working as expected + """ + data = { + 'attrib': 2, + 'measures': [ + {'unit': -1, 'type': 9, 'value': 800}, + {'unit': -1, 'type': 10, 'value': 1200}, + {'unit': -1, 'type': 11, 'value': 860} + ], + 'date': 1409361740, + 'category': 1, + 'grpid': 111111111 + } + group = WithingsMeasureGroup(data) + for _type, type_id in WithingsMeasureGroup.MEASURE_TYPES: + assert hasattr(group, _type) + if _type == 'diastolic_blood_pressure': + self.assertEqual(getattr(group, _type), 80.0) + elif _type == 'systolic_blood_pressure': + self.assertEqual(getattr(group, _type), 120.0) + elif _type == 'heart_pulse': + self.assertEqual(getattr(group, _type), 86.0) + else: + self.assertEqual(getattr(group, _type), None) + + def test_is_ambiguous(self): + """ Test the is_ambiguous method """ + data = {'attrib': 0, 'measures': [], 'date': 1409361740, 'category': 1, + 'grpid': 111111111} + self.assertEqual(WithingsMeasureGroup(data).is_ambiguous(), False) + data['attrib'] = 1 + assert WithingsMeasureGroup(data).is_ambiguous() + data['attrib'] = 2 + self.assertEqual(WithingsMeasureGroup(data).is_ambiguous(), False) + data['attrib'] = 4 + assert WithingsMeasureGroup(data).is_ambiguous() + + def test_is_measure(self): + """ Test the is_measure method """ + data = {'attrib': 0, 'measures': [], 'date': 1409361740, 'category': 1, + 'grpid': 111111111} + assert WithingsMeasureGroup(data).is_measure() + data['category'] = 2 + self.assertEqual(WithingsMeasureGroup(data).is_measure(), False) + + def test_is_target(self): + """ Test the is_target method """ + data = {'attrib': 0, 'measures': [], 'date': 1409361740, 'category': 1, + 'grpid': 111111111} + self.assertEqual(WithingsMeasureGroup(data).is_target(), False) + data['category'] = 2 + assert WithingsMeasureGroup(data).is_target() + + def test_get_measure(self): + """ + Check that the get_measure function is working as expected + """ + data = { + 'attrib': 2, + 'measures': [ + {'unit': -2, 'type': 9, 'value': 8000}, + {'unit': 1, 'type': 10, 'value': 12}, + {'unit': 0, 'type': 11, 'value': 86} + ], + 'date': 1409361740, + 'category': 1, + 'grpid': 111111111 + } + group = WithingsMeasureGroup(data) + self.assertEqual(group.get_measure(9), 80.0) + self.assertEqual(group.get_measure(10), 120.0) + self.assertEqual(group.get_measure(11), 86.0) + self.assertEqual(group.get_measure(12), None) diff --git a/tests/test_withings_measures.py b/tests/test_withings_measures.py new file mode 100644 index 0000000..5f46e3f --- /dev/null +++ b/tests/test_withings_measures.py @@ -0,0 +1,30 @@ +import time +import unittest + +from withings import WithingsMeasureGroup, WithingsMeasures + +class TestWithingsMeasures(unittest.TestCase): + def test_withings_measures_init(self): + """ + Check that WithingsMeasures create groups correctly and that the + update time is parsed correctly + """ + data = { + 'updatetime': 1409596058, + 'measuregrps': [ + {'attrib': 2, 'date': 1409361740, 'category': 1, + 'measures': [{'unit': -1, 'type': 1, 'value': 860}], + 'grpid': 111111111}, + {'attrib': 2, 'date': 1409361740, 'category': 1, + 'measures': [{'unit': -2, 'type': 4, 'value': 185}], + 'grpid': 111111112} + ] + } + measures = WithingsMeasures(data) + self.assertEqual(type(measures), WithingsMeasures) + self.assertEqual(len(measures), 2) + self.assertEqual(type(measures[0]), WithingsMeasureGroup) + self.assertEqual(measures[0].weight, 86.0) + self.assertEqual(measures[1].height, 1.85) + self.assertEqual(time.mktime(measures.updatetime.timetuple()), + 1409596058) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c6a890f --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +envlist = pypy,py34,py33,py32,py27,py26 + +[testenv] +commands = coverage run --branch --source=withings setup.py test +deps = -r{toxinidir}/requirements/test.txt + +[testenv:pypy] +basepython = pypy + +[testenv:py34] +basepython = python3.4 + +[testenv:py33] +basepython = python3.3 + +[testenv:py32] +basepython = python3.2 + +[testenv:py27] +basepython = python2.7 + +[testenv:py26] +basepython = python2.6