|
1
|
|
|
import functools |
|
2
|
|
|
import hashlib |
|
3
|
|
|
from flask import jsonify, request, url_for, current_app, make_response, g |
|
4
|
|
|
from .rate_limit import RateLimit |
|
5
|
|
|
from .errors import too_many_requests, precondition_failed, not_modified |
|
6
|
|
|
|
|
7
|
|
|
|
|
8
|
|
|
def json(f): |
|
9
|
|
|
@functools.wraps(f) |
|
10
|
|
|
def wrapped(*args, **kwargs): |
|
11
|
|
|
rv = f(*args, **kwargs) |
|
12
|
|
|
status_or_headers = None |
|
13
|
|
|
headers = None |
|
14
|
|
|
if isinstance(rv, tuple): |
|
15
|
|
|
rv, status_or_headers, headers = rv + (None, ) * (3 - len(rv)) |
|
16
|
|
|
if isinstance(status_or_headers, (dict, list)): |
|
17
|
|
|
headers, status_or_headers = status_or_headers, None |
|
18
|
|
|
if not isinstance(rv, dict): |
|
19
|
|
|
rv = rv.to_json() |
|
20
|
|
|
rv = jsonify(rv) |
|
21
|
|
|
if status_or_headers is not None: |
|
22
|
|
|
rv.status_code = status_or_headers |
|
23
|
|
|
if headers is not None: |
|
24
|
|
|
rv.headers.extend(headers) |
|
25
|
|
|
return rv |
|
26
|
|
|
|
|
27
|
|
|
return wrapped |
|
28
|
|
|
|
|
29
|
|
|
|
|
30
|
|
|
def hit_count(f): |
|
31
|
|
|
@functools.wraps(f) |
|
32
|
|
|
def wrapped(*args, **kwargs): |
|
33
|
|
|
rv = f(*args, **kwargs) |
|
34
|
|
|
|
|
35
|
|
|
if current_app.config['TESTING']: |
|
36
|
|
|
from .rate_limit import FakeRedis |
|
37
|
|
|
redis = FakeRedis() |
|
38
|
|
|
else: # pragma: no cover |
|
39
|
|
|
from redis import Redis |
|
40
|
|
|
redis_host = current_app.config.get("REDIS_HOST", "localhost") |
|
41
|
|
|
redis_port = current_app.config.get("REDIS_PORT", 6379) |
|
42
|
|
|
redis_db = current_app.config.get("REDIS_DB", 0) |
|
43
|
|
|
redis = Redis(host=redis_host, port=redis_port, db=redis_db) |
|
44
|
|
|
|
|
45
|
|
|
key = 'hit-count%s' % (request.path) |
|
46
|
|
|
redis.incr(key) |
|
47
|
|
|
return rv |
|
48
|
|
|
|
|
49
|
|
|
return wrapped |
|
50
|
|
|
|
|
51
|
|
|
|
|
52
|
|
|
def rate_limit(limit, per, scope_func=lambda: request.remote_addr): |
|
|
|
|
|
|
53
|
|
|
def decorator(f): |
|
54
|
|
|
@functools.wraps(f) |
|
55
|
|
|
def wrapped(*args, **kwargs): |
|
56
|
|
|
if current_app.config['USE_RATE_LIMITS']: |
|
57
|
|
|
key = 'rate-limit/%s/%s/' % (f.__name__, scope_func()) |
|
58
|
|
|
limiter = RateLimit(key, limit, per) |
|
59
|
|
|
if not limiter.over_limit: |
|
60
|
|
|
rv = f(*args, **kwargs) |
|
61
|
|
|
else: |
|
62
|
|
|
rv = too_many_requests('You have exceeded your request rate') |
|
63
|
|
|
# rv = make_response(rv) |
|
64
|
|
|
g.headers = { |
|
65
|
|
|
'X-RateLimit-Remaining': str(limiter.remaining), |
|
66
|
|
|
'X-RateLimit-Limit': str(limiter.limit), |
|
67
|
|
|
'X-RateLimit-Reset': str(limiter.reset) |
|
68
|
|
|
} |
|
69
|
|
|
return rv |
|
70
|
|
|
else: |
|
71
|
|
|
return f(*args, **kwargs) |
|
72
|
|
|
|
|
73
|
|
|
return wrapped |
|
74
|
|
|
|
|
75
|
|
|
return decorator |
|
76
|
|
|
|
|
77
|
|
|
|
|
78
|
|
|
def paginate(max_per_page=10): |
|
79
|
|
|
def decorator(f): |
|
80
|
|
|
@functools.wraps(f) |
|
81
|
|
|
def wrapped(*args, **kwargs): |
|
82
|
|
|
page = request.args.get('page', 1, type=int) |
|
83
|
|
|
per_page = min( |
|
84
|
|
|
request.args.get('per_page', max_per_page, type=int), max_per_page) |
|
85
|
|
|
query = f(*args, **kwargs) |
|
86
|
|
|
p = query.paginate(page, per_page) |
|
87
|
|
|
pages = { |
|
88
|
|
|
'page': page, |
|
89
|
|
|
'per_page': per_page, |
|
90
|
|
|
'total': p.total, |
|
91
|
|
|
'pages': p.pages |
|
92
|
|
|
} |
|
93
|
|
|
if p.has_prev: |
|
94
|
|
|
pages['prev'] = url_for( |
|
95
|
|
|
request.endpoint, |
|
96
|
|
|
page=p.prev_num, |
|
97
|
|
|
per_page=per_page, |
|
98
|
|
|
_external=True, |
|
99
|
|
|
**kwargs) |
|
100
|
|
|
else: |
|
101
|
|
|
pages['prev'] = None |
|
102
|
|
|
if p.has_next: |
|
103
|
|
|
pages['next'] = url_for( |
|
104
|
|
|
request.endpoint, |
|
105
|
|
|
page=p.next_num, |
|
106
|
|
|
per_page=per_page, |
|
107
|
|
|
_external=True, |
|
108
|
|
|
**kwargs) |
|
109
|
|
|
else: |
|
110
|
|
|
pages['next'] = None |
|
111
|
|
|
pages['first'] = url_for( |
|
112
|
|
|
request.endpoint, |
|
113
|
|
|
page=1, |
|
114
|
|
|
per_page=per_page, |
|
115
|
|
|
_external=True, |
|
116
|
|
|
**kwargs) |
|
117
|
|
|
pages['last'] = url_for( |
|
118
|
|
|
request.endpoint, |
|
119
|
|
|
page=p.pages, |
|
120
|
|
|
per_page=per_page, |
|
121
|
|
|
_external=True, |
|
122
|
|
|
**kwargs) |
|
123
|
|
|
return jsonify({ |
|
124
|
|
|
'urls': [item.get_url() for item in p.items], |
|
125
|
|
|
'meta': pages |
|
126
|
|
|
}) |
|
127
|
|
|
|
|
128
|
|
|
return wrapped |
|
129
|
|
|
|
|
130
|
|
|
return decorator |
|
131
|
|
|
|
|
132
|
|
|
|
|
133
|
|
|
def cache_control(*directives): |
|
134
|
|
|
def decorator(f): |
|
135
|
|
|
@functools.wraps(f) |
|
136
|
|
|
def wrapped(*args, **kwargs): |
|
137
|
|
|
rv = f(*args, **kwargs) |
|
138
|
|
|
rv = make_response(rv) |
|
139
|
|
|
rv.headers['Cache-Control'] = ', '.join(directives) |
|
140
|
|
|
return rv |
|
141
|
|
|
|
|
142
|
|
|
return wrapped |
|
143
|
|
|
|
|
144
|
|
|
return decorator |
|
145
|
|
|
|
|
146
|
|
|
|
|
147
|
|
|
def no_cache(f): |
|
148
|
|
|
return cache_control('no-cache', 'no-store', 'max-age=0')(f) |
|
149
|
|
|
|
|
150
|
|
|
|
|
151
|
|
|
def etag(f): |
|
152
|
|
|
@functools.wraps(f) |
|
153
|
|
|
def wrapped(*args, **kwargs): |
|
154
|
|
|
# only for HEAD and GET requests |
|
155
|
|
|
assert request.method in ['HEAD', 'GET'],\ |
|
156
|
|
|
'@etag is only supported for GET requests' |
|
157
|
|
|
rv = f(*args, **kwargs) |
|
158
|
|
|
rv = make_response(rv) |
|
159
|
|
|
etag_str = '"' + hashlib.md5(rv.get_data()).hexdigest() + '"' |
|
160
|
|
|
rv.headers['ETag'] = etag_str |
|
161
|
|
|
if_match = request.headers.get('If-Match') |
|
162
|
|
|
if_none_match = request.headers.get('If-None-Match') |
|
163
|
|
|
if if_match: |
|
164
|
|
|
etag_list = [tag.strip() for tag in if_match.split(',')] |
|
165
|
|
|
if etag_str not in etag_list and '*' not in etag_list: |
|
166
|
|
|
rv = precondition_failed() |
|
167
|
|
|
elif if_none_match: |
|
168
|
|
|
etag_list = [tag.strip() for tag in if_none_match.split(',')] |
|
169
|
|
|
if etag_str in etag_list or '*' in etag_list: |
|
170
|
|
|
rv = not_modified() |
|
171
|
|
|
return rv |
|
172
|
|
|
|
|
173
|
|
|
return wrapped |
|
174
|
|
|
|