Skip to content

Commit 70a135f

Browse files
committed
ordering by multiple fields, limits and offsets, tests
1 parent 1d0c69f commit 70a135f

File tree

7 files changed

+202
-44
lines changed

7 files changed

+202
-44
lines changed

proxy_py/_settings.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,21 @@
2828
'PORT': 55555,
2929
}
3030

31+
_PROXY_PROVIDER_SERVER_API_CONFIG_FETCH_CONFIG = {
32+
'fields': ['address', 'protocol', 'auth_data', 'domain', 'port', 'last_check_time',
33+
'number_of_bad_checks', 'bad_proxy', 'uptime', 'response_time'],
34+
'filter_fields': ['last_check_time', 'protocol', 'number_of_bad_checks', 'bad_proxy', 'uptime',
35+
'response_time'],
36+
'order_by_fields': ['last_check_time', 'number_of_bad_checks', 'uptime', 'response_time'],
37+
'default_order_by_fields': ['response_time', ],
38+
}
39+
3140
PROXY_PROVIDER_SERVER_API_CONFIG = {
3241
'proxy': {
33-
'modelClass': ('models', 'Proxy'),
42+
'model_class': ('models', 'Proxy'),
3443
'methods': {
35-
'get': {
36-
'fields': ['address', 'protocol', 'auth_data', 'domain', 'port', 'last_check_time',
37-
'number_of_bad_checks', 'bad_proxy', 'uptime', 'response_time'],
38-
'filterFields': ['last_check_time', 'protocol', 'number_of_bad_checks', 'bad_proxy', 'uptime',
39-
'response_time'],
40-
'orderFields': ['last_check_time', 'number_of_bad_checks', 'uptime'],
41-
}
44+
'get': _PROXY_PROVIDER_SERVER_API_CONFIG_FETCH_CONFIG,
45+
'count': _PROXY_PROVIDER_SERVER_API_CONFIG_FETCH_CONFIG,
4246
}
4347
}
4448
}

server/requests_to_models/request.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@ def __init__(self, class_name):
44
self.class_name = class_name
55

66

7-
class GetRequest(Request):
8-
def __init__(self, class_name, fields: list=None):
9-
super(GetRequest, self).__init__(class_name)
7+
class FetchRequest(Request):
8+
def __init__(self, class_name, fields: list=None, order_by: list=None):
9+
super(FetchRequest, self).__init__(class_name)
1010
self.fields = fields if fields is not None else []
11+
self.order_by = order_by if order_by is not None else []
12+
self.limit = 0
13+
self.offset = 0
1114

15+
16+
class GetRequest(FetchRequest):
1217
@staticmethod
1318
def from_request(request: Request):
1419
return GetRequest(request.class_name)
20+
21+
22+
class CountRequest(FetchRequest):
23+
@staticmethod
24+
def from_request(request: Request):
25+
return CountRequest(request.class_name)

server/requests_to_models/request_executor.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,64 @@
11
from models import session
2-
from server.requests_to_models.request import Request, GetRequest
2+
from server.requests_to_models.request import Request, GetRequest, CountRequest, FetchRequest
33
import importlib
44

55

66
class RequestExecutor:
77
def execute(self, request: Request):
88
try:
9-
return {
10-
GetRequest: self._get,
11-
}[type(request)](request)
9+
if isinstance(request, FetchRequest):
10+
return self._fetch(request)
11+
# return {
12+
# # GetRequest: self._get,
13+
# # CountRequest: self._count
14+
# FetchRequest: self._fetch
15+
# }[type(request)](request)
1216
except BaseException as ex:
1317
raise ExecutionError(repr(ex))
1418

15-
def _get(self, request: GetRequest):
19+
def _fetch(self, request: FetchRequest):
1620
package = importlib.import_module(request.class_name[0])
1721
class_name = getattr(package, request.class_name[1])
1822

1923
# TODO: remove bad_proxy
2024

2125
queryset = session.query(class_name).filter(class_name.number_of_bad_checks == 0)
22-
result = []
2326

24-
for item in queryset:
25-
obj = {}
27+
result = {
28+
'count': queryset.count(),
29+
}
2630

27-
for field_name in request.fields:
28-
obj[field_name] = getattr(item, field_name)
31+
if type(request) is GetRequest:
32+
if request.order_by:
33+
queryset = queryset.order_by(*self.order_by_list_to_sqlalchemy(request.order_by, class_name))
2934

30-
result.append(obj)
35+
if request.limit > 0:
36+
queryset = queryset.limit(request.limit)
3137

32-
return {
33-
'data': result,
34-
'count': queryset.count(),
35-
'has_more': False,
36-
}
38+
if request.offset > 0:
39+
queryset = queryset.offset(request.offset)
40+
41+
data = []
42+
43+
for item in queryset:
44+
obj = {}
45+
46+
for field_name in request.fields:
47+
obj[field_name] = getattr(item, field_name)
48+
49+
data.append(obj)
50+
51+
result['data'] = data
52+
result['has_more'] = request.offset + request.limit < result['count']
53+
54+
return result
55+
56+
def order_by_list_to_sqlalchemy(self, order_by_fields: list, class_name):
57+
result = []
58+
for field in order_by_fields:
59+
result.append(getattr(class_name, field))
60+
61+
return result
3762

3863

3964
class ExecutionError(Exception):

server/requests_to_models/request_parser.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from server.requests_to_models.request import Request, GetRequest
1+
from server.requests_to_models.request import Request, GetRequest, CountRequest
22

33
import string
44
import copy
@@ -23,6 +23,11 @@ def parse(self, request: dict):
2323

2424
if key in self.COMMA_SEPARATED_KEYS:
2525
request[key] = self.comma_separated_field_to_list(request[key])
26+
if key in {'limit', 'offset'}:
27+
try:
28+
request[key] = int(request[key])
29+
except ValueError:
30+
raise ValidationError('Value of key "{}" should be integer'.format(key))
2631

2732
self.validate_value(key, request[key])
2833

@@ -32,7 +37,7 @@ def validate_value(self, key: str, value):
3237
if type(value) not in [str, int, list]:
3338
raise ValidationError('Value type should be string, integer or list')
3439

35-
if len(value) > self.MAXIMUM_VALUE_LENGTH:
40+
if type(value) in [str, list] and len(value) > self.MAXIMUM_VALUE_LENGTH:
3641
raise ValidationError(
3742
'Some value is too big. Maximum allowed length is {}'.format(self.MAXIMUM_VALUE_LENGTH))
3843

@@ -92,7 +97,7 @@ def parse_dict(self, req_dict):
9297

9398
config = self.config[req_dict['model']]
9499

95-
result_request = Request(config['modelClass'])
100+
result_request = Request(config['model_class'])
96101

97102
if 'method' not in req_dict:
98103
raise ParseError('You should specify "method"')
@@ -105,26 +110,52 @@ def parse_dict(self, req_dict):
105110
config = config['methods'][method]
106111

107112
return {
108-
'get': self._get,
113+
'get': self.method_get,
114+
'count': self.method_count,
109115
}[method](req_dict, config, result_request)
110116

111-
def _get(self, req_dict, config, result_request):
112-
fields = []
117+
def method_get(self, req_dict, config, result_request):
118+
result_request = GetRequest.from_request(result_request)
119+
result_request.fields = self.parse_fields(req_dict, config)
120+
result_request.order_by = self.parse_order_by_fields(req_dict, config)
121+
if 'limit' in req_dict:
122+
result_request.limit = req_dict['limit']
123+
if 'offset' in req_dict:
124+
result_request.offset = req_dict['offset']
113125

114-
if 'fields' not in req_dict:
115-
fields = copy.copy(config['fields'])
116-
else:
117-
for field in req_dict['fields']:
118-
if field not in config['fields']:
119-
raise ParseError("Field \"{}\" doesn't exist or isn't allowed".format(field))
126+
return result_request
120127

121-
fields.append(field)
122-
123-
result_request = GetRequest.from_request(result_request)
124-
result_request.fields = fields
128+
def method_count(self, req_dict, config, result_request):
129+
result_request = CountRequest.from_request(result_request)
130+
result_request.fields = self.parse_fields(req_dict, config)
131+
result_request.order_by = self.parse_order_by_fields(req_dict, config)
132+
if 'limit' in req_dict:
133+
result_request.limit = req_dict['limit']
134+
if 'offset' in req_dict:
135+
result_request.offset = req_dict['offset']
125136

126137
return result_request
127138

139+
def parse_fields(self, req_dict, config):
140+
return self.parse_list(req_dict, config, "fields", "fields", config['fields'])
141+
142+
def parse_order_by_fields(self, req_dict, config):
143+
return self.parse_list(req_dict, config, "order_by", "order_by_fields", config['default_order_by_fields'])
144+
145+
def parse_list(self, req_dict, config, request_key, config_key, default_value):
146+
if request_key not in req_dict:
147+
return copy.copy(default_value)
148+
149+
result = []
150+
151+
for field in req_dict[request_key]:
152+
if field not in config[config_key]:
153+
raise ParseError("Field \"{}\" doesn't exist or isn't allowed".format(field))
154+
155+
result.append(field)
156+
157+
return result
158+
128159
def _validate_config(self):
129160
# TODO: check fields for existence and so on
130161
if False:

test

Lines changed: 0 additions & 1 deletion
This file was deleted.

tests/test_api.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import asyncio
2+
import aiohttp
3+
import json
4+
5+
6+
API_URL = "http://localhost:55555"
7+
8+
9+
async def get_proxies(session, request):
10+
async with session.post(API_URL, json=request) as resp:
11+
json_data = json.loads(await resp.text())
12+
return json_data['data']
13+
14+
15+
async def test_ordering(session, field_name):
16+
request_data = {
17+
'method': 'get',
18+
'model': 'proxy',
19+
'order_by': field_name
20+
}
21+
22+
previous_proxy = None
23+
24+
for proxy in await get_proxies(session, request_data):
25+
if previous_proxy is not None:
26+
if previous_proxy[field_name] > proxy[field_name]:
27+
return False
28+
29+
previous_proxy = proxy
30+
31+
return True
32+
33+
34+
async def test_complex_ordering(session, *args):
35+
fields = args
36+
37+
request_data = {
38+
'method': 'get',
39+
'model': 'proxy',
40+
'order_by': ', '.join(fields)
41+
}
42+
43+
previous_proxy = None
44+
45+
for proxy in await get_proxies(session, request_data):
46+
if previous_proxy is not None:
47+
for i in range(1, len(fields)):
48+
previous_field = fields[i - 1]
49+
field = fields[i]
50+
51+
if previous_proxy[previous_field] > proxy[previous_field]:
52+
return False
53+
elif previous_proxy[previous_field] < proxy[previous_field]:
54+
break
55+
56+
previous_proxy = proxy
57+
58+
return True
59+
60+
61+
async def run_tests(session):
62+
tests = [
63+
(test_ordering, 'response_time'),
64+
(test_ordering, 'uptime'),
65+
(test_ordering, 'number_of_bad_checks'),
66+
(test_ordering, 'last_check_time'),
67+
(test_complex_ordering, 'uptime', 'last_check_time'),
68+
(test_complex_ordering, 'number_of_bad_checks', 'uptime', 'response_time'),
69+
]
70+
71+
for test in tests:
72+
try:
73+
result = await test[0](session, *test[1:])
74+
print("PASSED" if result else "FAILED", end='')
75+
except BaseException as ex:
76+
print("FAILED DUE TO EXCEPTION: {}".format(ex), end='')
77+
finally:
78+
print(" test {} ".format(test))
79+
80+
81+
async def main():
82+
async with aiohttp.ClientSession() as session:
83+
await run_tests(session)
84+
85+
86+
if __name__ == '__main__':
87+
loop = asyncio.get_event_loop()
88+
loop.run_until_complete(main())
File renamed without changes.

0 commit comments

Comments
 (0)