-
Notifications
You must be signed in to change notification settings - Fork 5
Add zero-storage #16
Add zero-storage #16
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,37 @@ if (auth_status.pending == false and auth_status.granted == true): | |
#### Handling Errors | ||
If any request runs into an error a `ToopherApiError` will be thrown with more details on what went wrong. | ||
|
||
#### Zero-Storage usage option | ||
Requesters can choose to integrate the Toopher API in a way does not require storing any per-user data such as Pairing ID and Terminal ID - all of the storage | ||
is handled by the Toopher API Web Service, allowing your local database to remain unchanged. If the Toopher API needs more data, it will `die()` with a specific | ||
error string that allows your code to respond appropriately. | ||
|
||
```python | ||
try: | ||
# optimistically try to authenticate against Toopher API with username and a Terminal Identifier | ||
# Terminal Identifer is typically a randomly generated secure browser cookie. It does not | ||
# need to be human-readable | ||
auth = api.authenticate_by_user_name("[email protected]", "<terminal identifier>") | ||
|
||
# if you got here, everything is good! poll the auth request status as described above | ||
# there are four distinct errors ToopherAPI can return if it needs more data | ||
except UserDisabledError: | ||
# you have marked this user as disabled in the Toopher API. | ||
except UnknownUserError: | ||
# This user has not yet paired a mobile device with their account. Pair them | ||
# using api.pair() as described above, then re-try authentication | ||
except UnknownTerminalError: | ||
# This user has not assigned a "Friendly Name" to this terminal identifier. | ||
# Prompt them to enter a terminal name, then submit that "friendly name" to | ||
# the Toopher API: | ||
# api.assign_user_friendly_name_to_terminal(user_name, terminal_friendly_name, terminal_identifier) | ||
# Afterwards, re-try authentication | ||
except PairingDeactivatedError: | ||
# this user does not have an active pairing, | ||
# typically because they deleted the pairing. You can prompt | ||
# the user to re-pair with a new mobile device. | ||
``` | ||
|
||
#### Dependencies | ||
This library uses the python-oauth2 library to handle OAuth signing and httplib2 to make the web requests. If you install using pip (or easy_install) they'll be installed automatically for you. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import unittest | ||
import json | ||
import os | ||
import urlparse | ||
|
||
import toopher | ||
import unittest | ||
import urlparse | ||
|
||
class HttpClientMock(object): | ||
def __init__(self, paths): | ||
|
@@ -177,6 +177,56 @@ def test_access_arbitrary_keys_in_authentication_status(self): | |
|
||
self.assertEqual(auth_request.random_key, "84") | ||
|
||
def test_disabled_user_raises_correct_error(self): | ||
api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') | ||
api.client = HttpClientMock({ | ||
'https://toopher.test/v1/authentication_requests/initiate': | ||
({'status': 409}, json.dumps( | ||
{'error_code': 704, | ||
'error_message': 'disabled user'}))}) | ||
with self.assertRaises(toopher.UserDisabledError): | ||
auth_request = api.authenticate_by_user_name('disabled user', 'terminal name') | ||
|
||
def test_unknown_user_raises_correct_error(self): | ||
api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') | ||
api.client = HttpClientMock({ | ||
'https://toopher.test/v1/authentication_requests/initiate': | ||
({'status': 409}, json.dumps( | ||
{'error_code': 705, | ||
'error_message': 'unknown user'}))}) | ||
with self.assertRaises(toopher.UserUnknownError): | ||
auth_request = api.authenticate_by_user_name('unknown user', 'terminal name') | ||
|
||
def test_unknown_terminal_raises_correct_error(self): | ||
api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') | ||
api.client = HttpClientMock({ | ||
'https://toopher.test/v1/authentication_requests/initiate': | ||
({'status': 409}, json.dumps( | ||
{'error_code': 706, | ||
'error_message': 'unknown terminal'}))}) | ||
with self.assertRaises(toopher.TerminalUnknownError): | ||
auth_request = api.authenticate_by_user_name('user', 'unknown terminal name') | ||
|
||
def test_disabled_pairing_raises_correct_error(self): | ||
api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') | ||
api.client = HttpClientMock({ | ||
'https://toopher.test/v1/authentication_requests/initiate': | ||
({'status': 409}, json.dumps( | ||
{'error_code': 601, | ||
'error_message': 'pairing has been deactivated'}))}) | ||
with self.assertRaises(toopher.PairingDeactivatedError): | ||
auth_request = api.authenticate_by_user_name('user', 'terminal name') | ||
|
||
def test_disabled_pairing_raises_correct_error(self): | ||
api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') | ||
api.client = HttpClientMock({ | ||
'https://toopher.test/v1/authentication_requests/initiate': | ||
({'status': 409}, json.dumps( | ||
{'error_code': 601, | ||
'error_message': 'pairing has not been authorized'}))}) | ||
with self.assertRaises(toopher.PairingDeactivatedError): | ||
auth_request = api.authenticate_by_user_name('user', 'terminal name') | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you test the
|
||
def main(): | ||
unittest.main() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,14 @@ | |
DEFAULT_BASE_URL = "https://api.toopher.com/v1" | ||
VERSION = "1.0.6" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Psssst, bump up the VERSION to |
||
|
||
class ToopherApiError(Exception): pass | ||
class UserDisabledError(ToopherApiError): pass | ||
class UserUnknownError(ToopherApiError): pass | ||
class TerminalUnknownError(ToopherApiError): pass | ||
class PairingDeactivatedError(ToopherApiError): pass | ||
error_codes_to_errors = {704: UserDisabledError, | ||
705: UserUnknownError, | ||
706: TerminalUnknownError} | ||
|
||
class ToopherApi(object): | ||
def __init__(self, key, secret, api_url=None): | ||
|
@@ -25,7 +33,7 @@ def pair(self, pairing_phrase, user_name, **kwargs): | |
return PairingStatus(result) | ||
|
||
def pair_sms(self, phone_number, user_name, phone_country=None): | ||
uri = BASE_URL + "/pairings/create/sms" | ||
uri = self.base_url + "/pairings/create/sms" | ||
params = {'phone_number': phone_number, | ||
'user_name': user_name} | ||
|
||
|
@@ -60,30 +68,62 @@ def get_authentication_status(self, authentication_request_id): | |
return AuthenticationStatus(result) | ||
|
||
def authenticate_with_otp(self, authentication_request_id, otp): | ||
uri = BASE_URL + "/authentication_requests/" + authentication_request_id + '/otp_auth' | ||
uri = self.base_url + "/authentication_requests/" + authentication_request_id + '/otp_auth' | ||
params = {'otp' : otp} | ||
result = self._request(uri, "POST", params) | ||
return AuthenticationStatus(result) | ||
|
||
def authenticate_by_user_name(self, user_name, terminal_name_extra, action_name=None, **kwargs): | ||
kwargs.update(user_name=user_name, terminal_name_extra=terminal_name_extra) | ||
return self.authenticate('', '', action_name, **kwargs) | ||
|
||
def create_user_terminal(self, user_name, terminal_name, requester_terminal_id): | ||
uri = self.base_url + '/user_terminals/create' | ||
params = {'user_name': user_name, | ||
'name': terminal_name, | ||
'name_extra': requester_terminal_id} | ||
result = self._request(uri, 'POST', params) | ||
|
||
def set_enable_toopher_for_user(self, user_name, enabled): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Toopher Perl library called this method |
||
uri = self.base_url + '/users' | ||
users = self._request(uri, 'GET') | ||
if len(users) > 1: | ||
raise ToopherApiException('Multiple users with name = {}'.format(user_name)) | ||
elif not len(users): | ||
raise ToopherApiException('No users with name = {}'.format(user_name)) | ||
|
||
uri = self.base_url + '/users/' + users[0]['id'] | ||
params = {'disable_toopher_auth': bool(enabled)} | ||
result = self._request(uri, 'POST', params) | ||
|
||
def _request(self, uri, method, params=None): | ||
data = urllib.urlencode(params or {}) | ||
header_data = {'User-Agent':'Toopher-Python/{} (Python {})'.format(VERSION, sys.version.split()[0])} | ||
|
||
resp, content = self.client.request(uri, method, data, headers=header_data) | ||
if resp['status'] != '200': | ||
try: | ||
error_message = json.loads(content)['error_message'] | ||
except Exception: | ||
error_message = content | ||
raise ToopherApiError(error_message) | ||
|
||
response, content = self.client.request(uri, method, data, headers=header_data) | ||
try: | ||
result = json.loads(content) | ||
except Exception, e: | ||
raise ToopherApiError("Response from server could not be decoded as JSON: %s" % e) | ||
content = json.loads(content) | ||
except ValueError: | ||
raise ToopherApiError('Response from server could not be decoded as JSON.') | ||
|
||
if int(response['status']) > 300: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is |
||
self._parse_request_error(content) | ||
|
||
return content | ||
|
||
return result | ||
def _parse_request_error(self, content): | ||
error_code = content['error_code'] | ||
error_message = content['error_message'] | ||
if error_code in error_codes_to_errors: | ||
error = error_codes_to_errors[error_code] | ||
raise error(error_message) | ||
|
||
# TODO: Add an error code for PairingDeactivatedError. | ||
if ('pairing has been deactivated' in error_message | ||
or 'pairing has not been authorized' in error_message): | ||
raise PairingDeactivatedError(error_message) | ||
|
||
raise ToopherApiError(error_message) | ||
|
||
class PairingStatus(object): | ||
def __init__(self, json_response): | ||
|
@@ -128,7 +168,3 @@ def __nonzero__(self): | |
|
||
def __getattr__(self, name): | ||
return self._raw_data[name] | ||
|
||
|
||
class ToopherApiError(Exception): pass | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is now called
create_user_terminal