Skip to content

Commit 390c42d

Browse files
committed
Merge pull request #8 from hannseman/master
Add access to APIKey object, repeated url parameters, django <1.5 fixes
2 parents 9173faf + 153dc9d commit 390c42d

File tree

11 files changed

+84
-95
lines changed

11 files changed

+84
-95
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ include LICENSE
33
include README.rst
44
recursive-include formapi/templates *
55
recursive-include formapi/static *
6+
recursive-exclude formapi/tests *

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ test:
22
python setup.py test
33

44
flake8:
5-
flake8 --ignore=E501,E225,E128,W391,W404,W402 --exclude migrations --max-complexity 12 formapi
5+
flake8 --ignore=E501,E128 --exclude migrations --max-complexity 12 formapi
66

77
install:
88
python setup.py install

formapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = (0, 0, 7, 'dev')
1+
VERSION = (0, 1, 0, 'dev')
22

33
# Dynamically calculate the version based on VERSION tuple
44
if len(VERSION) > 2 and VERSION[2] is not None:

formapi/api.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class DjangoJSONEncoder(JSONEncoder):
4343

4444
def default(self, obj):
4545
date_obj = self.default_date(obj)
46-
if date_obj:
46+
if date_obj is not None:
4747
return date_obj
4848
elif isinstance(obj, decimal.Decimal):
4949
return str(obj)
@@ -75,6 +75,8 @@ def default_date(self, obj):
7575
if obj.microsecond:
7676
r = r[:12]
7777
return r
78+
elif isinstance(obj, datetime.timedelta):
79+
return obj.seconds
7880

7981
dumps = curry(dumps, cls=DjangoJSONEncoder)
8082

@@ -100,14 +102,19 @@ def get_form_class(self):
100102
except KeyError:
101103
raise Http404
102104

105+
def get_form_kwargs(self):
106+
kwargs = super(API, self).get_form_kwargs()
107+
if self.api_key:
108+
kwargs['api_key'] = self.api_key
109+
return kwargs
110+
103111
def get_access_params(self):
104112
key = self.request.REQUEST.get('key')
105113
sign = self.request.REQUEST.get('sign')
106114
return key, sign
107115

108116
def sign_ok(self, sign):
109-
pairs = ((field, self.request.REQUEST.get(field))
110-
for field in sorted(self.get_form_class()().fields.keys()))
117+
pairs = self.normalized_parameters()
111118
filtered_pairs = itertools.ifilter(lambda x: x[1] is not None, pairs)
112119
query_string = '&'.join(('='.join(pair) for pair in filtered_pairs))
113120
query_string = urllib2.quote(query_string.encode('utf-8'))
@@ -117,6 +124,20 @@ def sign_ok(self, sign):
117124
sha1).hexdigest()
118125
return constant_time_compare(sign, digest)
119126

127+
def normalized_parameters(self):
128+
"""
129+
Normalize django request to key value pairs sorted by key first and then value
130+
"""
131+
for field in sorted(self.get_form(self.get_form_class()).fields.keys()):
132+
value = self.request.REQUEST.getlist(field) or None
133+
if not value:
134+
continue
135+
if len(value) == 1:
136+
yield field, value[0]
137+
else:
138+
for item in sorted(value):
139+
yield field, item
140+
120141
def render_to_json_response(self, context, **response_kwargs):
121142
data = dumps(context)
122143
response_kwargs['content_type'] = 'application/json'
@@ -191,6 +212,6 @@ def dispatch(self, request, *args, **kwargs):
191212
return super(API, self).dispatch(request, *args, **kwargs)
192213

193214
# Access denied
194-
self.log.info('Access Denied %s', self.request.REQUEST)
215+
self.log.warning('Access Denied %s', self.request.REQUEST)
195216

196217
return HttpResponse(status=401)

formapi/calls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
class APICall(forms.Form):
55

6+
def __init__(self, api_key=None, *args, **kwargs):
7+
super(APICall, self).__init__(*args, **kwargs)
8+
self.api_key = api_key
9+
610
def add_error(self, error_msg):
711
errors = self.non_field_errors()
812
errors.append(error_msg)

formapi/tests/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def setUp(self):
2323
self.user.set_password("rosebud")
2424
self.user.save()
2525
self.authenticate_url = '/api/v1.0.0/user/authenticate/'
26+
self.language_url = '/api/v1.0.0/comp/lang/'
2627

2728
def send_request(self, url, data, key=None, secret=None, req_method="POST"):
2829
if not key:
@@ -42,8 +43,8 @@ def test_api_key(self):
4243

4344
def test_valid_auth(self):
4445
response = self.send_request(self.authenticate_url, {'username': self.user.username, 'password': 'rosebud'})
45-
response_data = json.loads(response.content)
4646
self.assertEqual(response.status_code, 200)
47+
response_data = json.loads(response.content)
4748
self.assertEqual(response_data['errors'], {})
4849
self.assertTrue(response_data['success'])
4950
self.assertIsNotNone(response_data['data'])
@@ -66,7 +67,7 @@ def test_invalid_sign(self):
6667
self.assertEqual(response.status_code, 401)
6768

6869
def test_invalid_password(self):
69-
data = {'username': self.user.username, 'password': '1337haxx'}
70+
data = {'username': self.user.username, 'password': '1337hax/x'}
7071
response = self.send_request(self.authenticate_url, data)
7172
self.assertEqual(response.status_code, 400)
7273
response_data = json.loads(response.content)
@@ -89,6 +90,11 @@ def test_get_call(self):
8990
response = self.send_request(self.authenticate_url, data, req_method='GET')
9091
self.assertEqual(response.status_code, 200)
9192

93+
def test_multiple_values(self):
94+
data = {'languages': ['python', 'java']}
95+
response = self.send_request(self.language_url, data, req_method='GET')
96+
self.assertEqual(response.status_code, 200)
97+
9298

9399
class HMACTest(TransactionTestCase):
94100

formapi/tests/calls.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,22 @@ def action(self, test):
7070
except ZeroDivisionError:
7171
self.add_error("DIVISION BY ZERO, OH SHIIIIII")
7272

73-
API.register(AuthenticateUserCall, 'user', 'authenticate', version='v1.0.0')
74-
API.register(DivisionCall, 'math', 'divide', version='v1.0.0')
7573

74+
class ProgrammingLanguages(calls.APICall):
75+
RUBY = 'ruby'
76+
PYTHON = 'python'
77+
JAVA = 'java'
78+
LANGUAGES = (
79+
(RUBY, 'Freshman'),
80+
(PYTHON, 'Sophomore'),
81+
(JAVA, 'Junior')
82+
)
83+
languages = forms.MultipleChoiceField(choices=LANGUAGES)
84+
85+
def action(self, test):
86+
return u'Good for you'
7687

7788

89+
API.register(AuthenticateUserCall, 'user', 'authenticate', version='v1.0.0')
90+
API.register(DivisionCall, 'math', 'divide', version='v1.0.0')
91+
API.register(ProgrammingLanguages, 'comp', 'lang', version='v1.0.0')

formapi/tests/urls.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,4 @@
33
except ImportError:
44
from django.conf.urls.defaults import patterns, url, include
55

6-
7-
urlpatterns = patterns('',
8-
url(r'^api/', include('formapi.urls')),
9-
)
6+
urlpatterns = patterns('', url(r'^api/', include('formapi.urls')))

formapi/utils.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
import hmac
22
import urllib2
33
from hashlib import sha1
4-
from django.utils.encoding import force_unicode
4+
from django.utils.encoding import smart_str, force_unicode
55

66

77
def get_sign(secret, querystring=None, **params):
88
"""
99
Return sign for querystring.
1010
1111
Logic:
12-
- Sort querystring by parameter keys
12+
- Sort querystring by parameter keys and by value if two or more parameter keys share the same name
1313
- URL encode sorted querystring
1414
- Generate a hex digested hmac/sha1 hash using given secret
1515
"""
1616
if querystring:
1717
params = dict(param.split('=') for param in querystring.split('&'))
18-
sorted_params = ((key, params[key]) for key in sorted(params.keys()))
18+
sorted_params = []
19+
for key, value in sorted(params.items(), key=lambda x: x[0]):
20+
if isinstance(value, basestring):
21+
sorted_params.append((key, value))
22+
else:
23+
try:
24+
value = list(value)
25+
except TypeError, e:
26+
assert 'is not iterable' in str(e)
27+
value = smart_str(value)
28+
sorted_params.append((key, value))
29+
else:
30+
sorted_params.extend((key, item) for item in sorted(value))
1931
param_list = ('='.join((field, force_unicode(value))) for field, value in sorted_params)
20-
validation_string = force_unicode('&'.join(param_list))
21-
return hmac.new(str(secret), urllib2.quote(validation_string.encode('utf-8')), sha1).hexdigest()
32+
validation_string = smart_str('&'.join(param_list))
33+
validation_string = urllib2.quote(validation_string)
34+
return hmac.new(str(secret), validation_string, sha1).hexdigest()

formapi/views.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,3 @@ def call(request, version, namespace, call_name):
1717
'docstring': form_class.__doc__
1818
}
1919
return render(request, 'formapi/api/call.html', context)
20-

0 commit comments

Comments
 (0)