Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #24 from alphagov/sort_and_limit

Sort and limit
  • Loading branch information...
commit 694427e4b003f070a205f95739741245dc96a7b6 2 parents 7680db6 + c513fb5
@pbadenski pbadenski authored
View
49 backdrop/core/bucket.py
@@ -26,31 +26,39 @@ def _period_group(self, doc):
'_count': doc['_count']
}
- def execute_weekly_group_query(self, key2, query):
- key1 = '_week_start_at'
+ def execute_weekly_group_query(self, group_by, query, sort=None,
+ limit=None):
+ period_key = '_week_start_at'
result = []
- cursor = self.repository.multi_group(key1, key2, query)
+ cursor = self.repository.multi_group(
+ group_by, period_key, query, sort=sort, limit=limit)
for doc in cursor:
- week_start_at = utc(doc.pop('_week_start_at'))
- doc['_start_at'] = week_start_at
- doc['_end_at'] = week_start_at + datetime.timedelta(days=7)
+ doc['values'] = doc.pop('_subgroup')
+
+ for item in doc['values']:
+ start_at = utc(item.pop("_week_start_at"))
+ item.update({
+ "_start_at": start_at,
+ "_end_at": start_at + datetime.timedelta(days=7)
+ })
+
result.append(doc)
return result
- def execute_grouped_query(self, group_by, query):
- cursor = self.repository.group(group_by, query)
+ def execute_grouped_query(self, group_by, query, sort=None, limit=None):
+ cursor = self.repository.group(group_by, query, sort, limit)
result = [{group_by: doc[group_by], '_count': doc['_count']} for doc
in cursor]
return result
- def execute_period_query(self, query):
- cursor = self.repository.group('_week_start_at', query)
+ def execute_period_query(self, query, limit=None):
+ cursor = self.repository.group('_week_start_at', query, limit=limit)
result = [self._period_group(doc) for doc in cursor]
return result
- def execute_query(self, query):
+ def execute_query(self, query, sort=None, limit=None):
result = []
- cursor = self.repository.find(query)
+ cursor = self.repository.find(query, sort=sort, limit=limit)
for doc in cursor:
# stringify the id
doc['_id'] = str(doc['_id'])
@@ -61,14 +69,19 @@ def execute_query(self, query):
def query(self, **params):
query = build_query(**params)
+ sort_by = params.get('sort_by')
+ group_by = params.get('group_by')
+ limit = params.get('limit')
- if 'group_by' in params and 'period' in params:
- result = self.execute_weekly_group_query(params['group_by'], query)
- elif 'group_by' in params:
- result = self.execute_grouped_query(params['group_by'], query)
+ if group_by and 'period' in params:
+ result = self.execute_weekly_group_query(
+ group_by, query, sort_by, limit)
+ elif group_by:
+ result = self.execute_grouped_query(
+ group_by, query, sort_by, limit)
elif 'period' in params:
- result = self.execute_period_query(query)
+ result = self.execute_period_query(query, limit)
else:
- result = self.execute_query(query)
+ result = self.execute_query(query, sort_by, limit)
return result
View
140 backdrop/core/database.py
@@ -1,7 +1,5 @@
-from itertools import groupby
from bson import Code
-from pprint import pprint
-from pymongo import MongoClient
+import pymongo
def build_query(**params):
@@ -26,7 +24,7 @@ def ensure_has_timestamp(q):
class Database(object):
def __init__(self, host, port, name):
- self._mongo = MongoClient(host, port)
+ self._mongo = pymongo.MongoClient(host, port)
self.name = name
def alive(self):
@@ -48,34 +46,45 @@ def __init__(self, collection):
def name(self):
return self._collection.name
- def find(self, query):
- return self._collection.find(query).sort('_timestamp', -1)
-
- def group(self, group_by, query):
- return self._group([group_by], query)
+ def _validate_sort(self, sort):
+ if len(sort) != 2:
+ raise InvalidSortError("Expected a key and direction")
+
+ if sort[1] not in ["ascending", "descending"]:
+ raise InvalidSortError(sort[1])
+
+ def find(self, query, sort=None, limit=None):
+ cursor = self._collection.find(query)
+ if sort:
+ self._validate_sort(sort)
+ else:
+ sort = ["_timestamp", "ascending"]
+ sort_options = {
+ "ascending": pymongo.ASCENDING,
+ "descending": pymongo.DESCENDING
+ }
+ cursor.sort(sort[0], sort_options[sort[1]])
+ if limit:
+ cursor.limit(limit)
+
+ return cursor
+
+ def group(self, group_by, query, sort=None, limit=None):
+ if sort:
+ self._validate_sort(sort)
+ return self._group([group_by], query, sort, limit)
def save(self, obj):
self._collection.save(obj)
- def multi_group(self, key1, key2, query):
+ def multi_group(self, key1, key2, query, sort=None, limit=None):
if key1 == key2:
raise GroupingError("Cannot group on two equal keys")
- results = self._group([key1, key2], query)
-
- output = nested_merge([key1, key2], results)
-
- result = []
- for key1_value, value in sorted(output.items()):
- result.append({
- key1: key1_value,
- "_count": sum(doc['_count'] for doc in value.values()),
- "_group_count": len(value),
- key2: value
- })
+ results = self._group([key1, key2], query, sort, limit)
- return result
+ return results
- def _group(self, keys, query):
+ def _group(self, keys, query, sort=None, limit=None):
results = self._collection.group(
key=keys,
condition=query,
@@ -88,6 +97,22 @@ def _group(self, keys, query):
for key in keys:
if result[key] is None:
return []
+
+ results = nested_merge(keys, results)
+
+ if sort:
+ sorters = {
+ "ascending": lambda a, b: cmp(a, b),
+ "descending": lambda a, b: cmp(b, a)
+ }
+ sorter = sorters[sort[1]]
+ try:
+ results.sort(cmp=sorter, key=lambda a: a[sort[0]])
+ except KeyError:
+ raise InvalidSortError('Invalid sort key {0}'.format(sort[0]))
+ if limit:
+ results = results[:limit]
+
return results
@@ -95,19 +120,64 @@ class GroupingError(ValueError):
pass
+class InvalidSortError(ValueError):
+ pass
+
+
def nested_merge(keys, results):
- output = {}
+ groups = []
for result in results:
- output = _inner_merge(output, keys, result)
- return output
+ groups = _merge(groups, keys, result)
+ return groups
+
+
+def _merge(groups, keys, result):
+ keys = list(keys)
+ key = keys.pop(0)
+ is_leaf = (len(keys) == 0)
+ value = result.pop(key)
+
+ group = _find_group(group for group in groups if group[key] == value)
+ if not group:
+ if is_leaf:
+ group = _new_leaf_node(key, value, result)
+ else:
+ group = _new_branch_node(key, value)
+ groups.append(group)
+
+ if not is_leaf:
+ _merge_and_sort_subgroup(group, keys, result)
+ _add_branch_node_counts(group)
+ return groups
+
+
+def _find_group(items):
+ """Return the first item in an iterator or None"""
+ try:
+ return next(items)
+ except StopIteration:
+ return
+
+
+def _new_branch_node(key, value):
+ """Create a new node that has further sub-groups"""
+ return {
+ key: value,
+ "_subgroup": []
+ }
+
+
+def _new_leaf_node(key, value, result):
+ """Create a new node that has no further sub-groups"""
+ result[key] = value
+ return result
+
+def _merge_and_sort_subgroup(group, keys, result):
+ group['_subgroup'] = _merge(group['_subgroup'], keys, result)
+ group['_subgroup'].sort(key=lambda d: d[keys[0]])
-def _inner_merge(output, keys, value):
- if len(keys) == 0:
- return value
- key = value.pop(keys[0])
- if key not in output:
- output[key] = {}
- output[key].update(_inner_merge(output[key], keys[1:], value))
- return output
+def _add_branch_node_counts(group):
+ group['_count'] = sum(doc.get('_count', 0) for doc in group['_subgroup'])
+ group['_group_count'] = len(group['_subgroup'])
View
18 backdrop/core/test_build_query.py
@@ -1,29 +1,29 @@
from unittest import TestCase
from hamcrest import *
from backdrop.core.database import build_query
-from tests.support.test_helpers import d
+from tests.support.test_helpers import d_tz
class TestBuild_query(TestCase):
def test_build_query_with_start_at(self):
- query = build_query(start_at = d(2013, 3, 18, 18, 10, 05))
+ query = build_query(start_at = d_tz(2013, 3, 18, 18, 10, 05))
assert_that(query, is_(
- {"_timestamp": {"$gte": d(2013, 03, 18, 18, 10, 05)}}))
+ {"_timestamp": {"$gte": d_tz(2013, 03, 18, 18, 10, 05)}}))
def test_build_query_with_end_at(self):
- query = build_query(end_at = d(2012, 3, 17, 17, 10, 6))
+ query = build_query(end_at = d_tz(2012, 3, 17, 17, 10, 6))
assert_that(query, is_(
- {"_timestamp": {"$lt": d(2012, 3, 17, 17, 10, 6)}}))
+ {"_timestamp": {"$lt": d_tz(2012, 3, 17, 17, 10, 6)}}))
def test_build_query_with_start_and_end_at(self):
query = build_query(
- start_at = d(2012, 3, 17, 17, 10, 6),
- end_at = d(2012, 3, 19, 17, 10, 6)
+ start_at = d_tz(2012, 3, 17, 17, 10, 6),
+ end_at = d_tz(2012, 3, 19, 17, 10, 6)
)
assert_that(query, is_({
"_timestamp": {
- "$gte": d(2012, 3, 17, 17, 10, 6),
- "$lt": d(2012, 3, 19, 17, 10, 6)
+ "$gte": d_tz(2012, 3, 17, 17, 10, 6),
+ "$lt": d_tz(2012, 3, 19, 17, 10, 6)
}
}))
View
6 backdrop/read/api.py
@@ -30,6 +30,7 @@ def parse_request_args(request_args):
if 'start_at' in request_args:
args['start_at'] = parse_time_string(request_args['start_at'])
+
if 'end_at' in request_args:
args['end_at'] = parse_time_string(request_args['end_at'])
@@ -44,6 +45,11 @@ def parse_request_args(request_args):
if 'group_by' in request_args:
args['group_by'] = request_args['group_by']
+ if 'sort_by' in request_args:
+ args['sort_by'] = request_args['sort_by'].split(':', 1)
+
+ if 'limit' in request_args:
+ args['limit'] = int(request_args['limit'])
return args
View
73 backdrop/read/validation.py
@@ -1,26 +1,75 @@
from ..core.validation import value_is_valid_datetime_string, valid, invalid
+MESSAGES = {
+ 'start_at': {
+ 'invalid': 'start_at is not a valid datetime'
+ },
+ 'end_at': {
+ 'invalid': 'end_at is not a valid datetime'
+ },
+ 'filter_by': {
+ 'colon': 'filter_by must be a field name and value separated by '
+ 'a colon (:) eg. authority:Westminster',
+ 'dollar': 'filter_by must not start with a $'
+ },
+ 'period': {
+ 'invalid': 'Unrecognised grouping for period. Supported periods '
+ 'include: week',
+ 'group': 'Cannot group on two equal keys',
+ 'sort': 'Period queries are sorted by time'
+ },
+ 'group_by': {
+ 'internal': 'Cannot group by internal fields, internal fields start '
+ 'with an underscore'
+ },
+ 'sort_by': {
+ 'colon': 'sort_by must be a field name and sort direction separated '
+ 'by a colon (:) eg. authority:ascending',
+ 'direction': 'Unrecognised sort direction. Supported directions '
+ 'include: ascending, descending'
+ },
+ 'limit': {
+ 'invalid': 'limit must be a positive integer'
+ }
+}
+
+
def validate_request_args(request_args):
if 'start_at' in request_args:
if not value_is_valid_datetime_string(request_args['start_at']):
- return invalid('start_at is not a valid datetime')
+ return invalid(MESSAGES['start_at']['invalid'])
if 'end_at' in request_args:
if not value_is_valid_datetime_string(request_args['end_at']):
- return invalid('end_at is not a valid datetime')
+ return invalid(MESSAGES['end_at']['invalid'])
if 'filter_by' in request_args:
if request_args['filter_by'].find(':') < 0:
- return invalid('filter_by is not valid')
- if request_args['filter_by'].startswith("$"):
- return invalid('filter_by is not valid')
+ return invalid(MESSAGES['filter_by']['colon'])
+ if request_args['filter_by'].startswith('$'):
+ return invalid(MESSAGES['filter_by']['dollar'])
if 'period' in request_args:
if request_args['period'] != 'week':
- return invalid('Unrecognized grouping for period')
- if "group_by" in request_args:
- if "_week_start_at" == request_args["group_by"]:
- return invalid('Cannot group on two equal keys')
- if "group_by" in request_args:
- if request_args["group_by"].startswith("_"):
- return invalid('Cannot group by internal fields')
+ return invalid(MESSAGES['period']['invalid'])
+ if 'group_by' in request_args:
+ if '_week_start_at' == request_args['group_by']:
+ return invalid(MESSAGES['period']['group'])
+ if 'sort_by' in request_args and 'group_by' not in request_args:
+ return invalid(MESSAGES['period']['sort'])
+ if 'group_by' in request_args:
+ if request_args['group_by'].startswith('_'):
+ return invalid(MESSAGES['group_by']['internal'])
+ if 'sort_by' in request_args:
+ if request_args['sort_by'].find(':') < 0:
+ return invalid(MESSAGES['sort_by']['colon'])
+ sort_order = request_args['sort_by'].split(':', 1)[1]
+ if sort_order not in ['ascending', 'descending']:
+ return invalid(MESSAGES['sort_by']['direction'])
+ if 'limit' in request_args:
+ try:
+ limit = int(request_args['limit'])
+ if limit < 0:
+ raise ValueError()
+ except ValueError:
+ return invalid(MESSAGES['limit']['invalid'])
return valid()
View
2  features/end_to_end.feature
@@ -13,4 +13,4 @@ Feature: end-to-end platform test
when I post the data to "/flavour_events"
and I go to "/flavour_events?period=week&group_by=flavour"
then I should get back a status of "200"
- and the JSON should have "3" result(s)
+ and the JSON should have "4" result(s)
View
7 features/fixtures/licensing_2.json
@@ -2,6 +2,7 @@
{
"_id": "1234",
"_timestamp": "2012-12-12T01:01:01+00:00",
+ "_week_start_at": "2012-12-10T00:00:00+00:00",
"licence_name": "Temporary events notice",
"interaction": "success",
@@ -11,6 +12,7 @@
{
"_id": "1235",
"_timestamp": "2012-12-12T01:01:01+00:00",
+ "_week_start_at": "2012-12-10T00:00:00+00:00",
"licence_name": "Temporary events notice",
"interaction": "success",
@@ -20,6 +22,7 @@
{
"_id": "1236",
"_timestamp": "2012-12-13T01:01:01+00:00",
+ "_week_start_at": "2012-12-10T00:00:00+00:00",
"licence_name": "Temporary events notice",
"interaction": "success",
@@ -29,6 +32,7 @@
{
"_id": "1237",
"_timestamp": "2012-12-14T01:01:01+00:00",
+ "_week_start_at": "2012-12-10T00:00:00+00:00",
"licence_name": "Temporary events notice",
"interaction": "success",
@@ -37,7 +41,8 @@
},
{
"_id": "1238",
- "_timestamp": "2012-12-14T01:01:01+00:00",
+ "_timestamp": "2012-12-04T01:01:01+00:00",
+ "_week_start_at": "2012-12-03T00:00:00+00:00",
"licence_name": "Cat herding licence",
"interaction": "success",
View
32 features/fixtures/sort_and_limit.json
@@ -0,0 +1,32 @@
+[
+ {
+ "name": "aardvark",
+ "value": 8,
+ "type": "wild"
+ },
+ {
+ "name": "buffalo",
+ "value": 7,
+ "type": "wild"
+ },
+ {
+ "name": "cat",
+ "value": 3,
+ "type": "domestic"
+ },
+ {
+ "name": "dog",
+ "value": 3,
+ "type": "domestic"
+ },
+ {
+ "name": "elephant",
+ "value": 8,
+ "type": "wild"
+ },
+ {
+ "name": "frog",
+ "value": 4,
+ "type": "wild"
+ }
+]
View
7 features/read_api.feature
@@ -18,7 +18,7 @@ Feature: the performance platform read api
when I go to "/foo?start_at=2012-12-13T01:01:01%2B00:00"
then I should get back a status of "200"
and the JSON should have "4" results
- and the "1st" result should be "{"_timestamp": "2012-12-19T01:01:01+00:00", "licence_name": "Temporary events notice", "interaction": "success", "authority": "Westminster", "type": "success", "_id": "1238"}"
+ and the "1st" result should be "{"_timestamp": "2012-12-13T01:01:01+00:00", "authority": "Westminster", "interaction": "success", "licence_name": "Temporary events notice", "_id": "1236", "type": "success"}"
Scenario: querying for data BEFORE a certain point
Given "licensing.json" is in "foo" bucket
@@ -88,7 +88,7 @@ Feature: the performance platform read api
when I go to "/weekly?period=week&group_by=name"
then I should get back a status of "200"
and the JSON should have "2" results
- and the "1st" result should be "{"_count": 3.0, "_group_count": 2.0, "_start_at": "2013-03-11T00:00:00+00:00", "_end_at" : "2013-03-18T00:00:00+00:00", "name": { "alpha": { "_count" : 2.0 }, "beta": { "_count" : 1.0 } } }"
+ and the "1st" result should have "values" with item "{"_start_at": "2013-03-11T00:00:00+00:00", "_end_at": "2013-03-18T00:00:00+00:00", "_count": 2.0}"
Scenario: grouping data by time period (week) and a name that doesn't exist
Given "stored_timestamps_for_filtering.json" is in "weekly" bucket
@@ -100,16 +100,13 @@ Feature: the performance platform read api
Given "licensing.json" is in "weekly" bucket
when I go to "/weekly?period=week&group_by=_week_start_at"
then I should get back a status of "400"
- and I should get back a message: "{ "status": "error", "message": "Cannot group on two equal keys" }"
Scenario: grouping data by internal fields is not allowed
Given "licensing.json" is in "weekly" bucket
when I go to "/weekly?group_by=_anything"
then I should get back a status of "400"
- and I should get back a message: "{ "status": "error", "message": "Cannot group by internal fields" }"
Scenario: filtering by a field name starting with "$" is not allowed because of security reasons
Given "licensing.json" is in "weekly" bucket
when I go to "/weekly?filter_by=$where:function(){}"
then I should get back a status of "400"
- and I should get back a message: "{ "status": "error", "message": "filter_by is not valid" }"
View
50 features/sort_and_limit.feature
@@ -0,0 +1,50 @@
+@use_read_api_client
+Feature: sorting and limiting
+
+ Scenario: Sort the data on a key that has a numeric value in ascending order
+ Given "sort_and_limit.json" is in "foo" bucket
+ when I go to "/foo?sort_by=value:ascending"
+ then I should get back a status of "200"
+ and the "1st" result should have "value" equaling the integer "3"
+ and the "last" result should have "value" equaling the integer "8"
+
+ Scenario: Sort the data on a key that has a numeric value in descending order
+ Given "sort_and_limit.json" is in "foo" bucket
+ when I go to "/foo?sort_by=value:descending"
+ then I should get back a status of "200"
+ and the "1st" result should have "value" equaling the integer "8"
+ and the "last" result should have "value" equaling the integer "3"
+
+ Scenario: Limit the data to first 3 elements
+ Given "sort_and_limit.json" is in "foo" bucket
+ when I go to "/foo?limit=3"
+ then I should get back a status of "200"
+ and the JSON should have "3" results
+
+ Scenario: Sort grouped query on a key and limit
+ Given "sort_and_limit.json" is in "foo" bucket
+ when I go to "/foo?group_by=type&sort_by=_count:ascending&limit=1"
+ then I should get back a status of "200"
+ and the JSON should have "1" result
+ and the "1st" result should have "type" equaling "domestic"
+
+ Scenario: Sort periodic grouped query on a key
+ Given "licensing_2.json" is in "foo" bucket
+ when I go to "/foo?group_by=authority&period=week&sort_by=_count:descending"
+ then I should get back a status of "200"
+ and the JSON should have "2" results
+ and the "1st" result should have "authority" equaling "Westminster"
+
+ Scenario: Sort periodic grouped query on a key and limit
+ Given "licensing_2.json" is in "foo" bucket
+ when I go to "/foo?group_by=authority&period=week&sort_by=_count:ascending&limit=1"
+ then I should get back a status of "200"
+ and the JSON should have "1" results
+ and the "1st" result should have "authority" equaling "Camden"
+
+ Scenario: Limit periodic query
+ Given "licensing_2.json" is in "foo" bucket
+ when I go to "/foo?period=week&limit=1"
+ then I should get back a status of "200"
+ and the JSON should have "1" result
+ and the "1st" result should have "_start_at" equaling "2012-12-10T00:00:00+00:00"
View
35 features/steps/read_api.py
@@ -53,9 +53,42 @@ def step(context, n):
step_matcher("parse")
+def parse_position(nth, data):
+ match = re.compile(r'\d+').match(nth)
+ if match:
+ return int(match.group(0)) - 1
+ elif nth == "last":
+ return len(data) - 1
+ elif nth == "first":
+ return 0
+ else:
+ raise IndexError(nth)
+
+
@then('the "{nth}" result should be "{expected_json}"')
def step(context, nth, expected_json):
- i = int(re.compile(r'\d+').match(nth).group(0)) - 1
the_data = json.loads(context.response.data)['data']
+ i = parse_position(nth, the_data)
expected = json.loads(expected_json)
assert_that(the_data[i], is_(expected))
+
+
+@then('the "{nth}" result should have "{key}" equaling "{value}"')
+def step(context, nth, key, value):
+ the_data = json.loads(context.response.data)['data']
+ i = parse_position(nth, the_data)
+ assert_that(the_data[i][key], equal_to(value))
+
+
+@then('the "{nth}" result should have "{key}" equaling the integer "{value}"')
+def step(context, nth, key, value):
+ the_data = json.loads(context.response.data)['data']
+ i = parse_position(nth, the_data)
+ assert_that(the_data[i][key], equal_to(int(value)))
+
+
+@then('the "{nth}" result should have "{key}" with item "{value}"')
+def step(context, nth, key, value):
+ the_data = json.loads(context.response.data)['data']
+ i = parse_position(nth, the_data)
+ assert_that(the_data[i][key], has_item(json.loads(value)))
View
4 jenkins.sh
@@ -30,8 +30,8 @@ nosetests -v --with-xunit --with-coverage --cover-package=backdrop --cover-inclu
display_result $? 1 "Unit tests"
python -m coverage.__main__ xml --include=backdrop*
-behave
+behave --tags=-wip --stop
display_result $? 2 "Feature tests"
-$(dirname $0)/pep-it.sh > pep8.out
+$(dirname $0)/pep-it.sh | tee pep8.out
display_result $? 3 "Code style check"
View
407 tests/core/integration/test_database_integration.py
@@ -0,0 +1,407 @@
+import unittest
+from abc import ABCMeta
+
+from hamcrest import *
+from pymongo import MongoClient
+
+from backdrop.core.database import Repository, GroupingError, \
+ InvalidSortError
+from tests.support.test_helpers import d, d_tz
+
+HOST = 'localhost'
+PORT = 27017
+DB_NAME = 'performance_platform_test'
+BUCKET = 'test_repository_integration'
+
+
+class RepositoryIntegrationTest(unittest.TestCase):
+ __metaclass__ = ABCMeta
+
+ def setUp(self):
+ self.repo = Repository(MongoClient(HOST, PORT)[DB_NAME][BUCKET])
+ self.mongo_collection = MongoClient(HOST, PORT)[DB_NAME][BUCKET]
+
+ def tearDown(self):
+ self.mongo_collection.drop()
+
+
+class TestRepositoryIntegration(RepositoryIntegrationTest):
+ def test_save(self):
+ thing_to_save = {'name': 'test_document'}
+ another_thing_to_save = {'name': '2nd_test_document'}
+
+ self.repo.save(thing_to_save)
+ self.repo.save(another_thing_to_save)
+
+ results = self.mongo_collection.find()
+ assert_that(results, has_item(thing_to_save))
+ assert_that(results, has_item(another_thing_to_save))
+
+ def test_save_updates_document_with_id(self):
+ a_document = {"_id": "event1", "title": "I'm an event"}
+ updated_document = {"_id": "event1", "title": "I'm another event"}
+
+ self.repo.save(a_document)
+ self.repo.save(updated_document)
+
+ saved_documents = self.mongo_collection.find()
+
+ assert_that( saved_documents, only_contains(updated_document) )
+
+ def test_find(self):
+ self.mongo_collection.save({"name": "George", "plays": "guitar"})
+ self.mongo_collection.save({"name": "John", "plays": "guitar"})
+ self.mongo_collection.save({"name": "Paul", "plays": "bass"})
+ self.mongo_collection.save({"name": "Ringo", "plays": "drums"})
+
+ results = self.repo.find({"plays": "guitar"})
+
+ assert_that(results, only_contains(
+ has_entries({"name": "George", "plays": "guitar"}),
+ has_entries({"name": "John", "plays": "guitar"}),
+ ))
+
+
+class TestRepositoryIntegration_Grouping(RepositoryIntegrationTest):
+ def setUp(self):
+ super(TestRepositoryIntegration_Grouping, self).setUp()
+ people = ["Jack", "Jill", "John", "Jane"]
+ places = ["Kettering", "Kew", "Kennington", "Kingston"]
+ times = [d_tz(2013, 3, 11), d_tz(2013, 3, 18), d_tz(2013, 3, 25)]
+
+ self._save_location("Jack", "Kettering", d_tz(2013, 3, 11))
+ self._save_location("Jill", "Kennington", d_tz(2013, 3, 25))
+ self._save_location("John", "Kettering", d_tz(2013, 3, 18))
+ self._save_location("John", "Kennington", d_tz(2013, 3, 11))
+ self._save_location("Jane", "Kingston", d_tz(2013, 3, 18))
+
+ def _save_location(self, person, place, time):
+ self.mongo_collection.save({
+ "person": person,
+ "place": place,
+ "_week_start_at": time
+ })
+
+ def tearDown(self):
+ super(TestRepositoryIntegration_Grouping, self).tearDown()
+
+ def test_group(self):
+ results = self.repo.group("place", {})
+
+ assert_that(results, only_contains(
+ has_entries({"place": "Kettering", "_count": 2}),
+ has_entries({"place": "Kennington", "_count": 2}),
+ has_entries({"place": "Kingston", "_count": 1})
+ ))
+
+ def test_group_with_query(self):
+ results = self.repo.group("place", {"person": "John"})
+
+ assert_that(results, only_contains(
+ {"place": "Kettering", "_count": 1},
+ {"place": "Kennington", "_count": 1}
+ ))
+
+ def test_key1_is_pulled_to_the_top_of_outer_group(self):
+ results = self.repo.multi_group("_week_start_at", "person", {})
+
+ assert_that(results, has_item(has_entry(
+ "_week_start_at", d(2013, 3, 11)
+ )))
+ assert_that(results, has_item(has_entry(
+ "_week_start_at", d(2013, 3, 25)
+ )))
+
+ def test_should_use_second_key_for_inner_group_name(self):
+ results = self.repo.multi_group("_week_start_at", "person", {})
+
+ assert_that(results, has_item(has_entry(
+ "_subgroup", has_item(has_entry("person", "Jill"))
+ )))
+
+ def test_count_of_outer_elements_should_be_added(self):
+ results = self.repo.multi_group("_week_start_at", "person", {})
+
+ assert_that(results, has_item(has_entry(
+ "_count", 1
+ )))
+
+ def test_grouping_by_multiple_keys(self):
+ results = self.repo.multi_group("person", "place", {})
+
+ assert_that(results, has_item({
+ "person": "Jack",
+ "_count": 1,
+ "_group_count": 1,
+ "_subgroup": [
+ { "place": "Kettering", "_count": 1 }
+ ]
+ }))
+ assert_that(results, has_item({
+ "person": "Jill",
+ "_count": 1,
+ "_group_count": 1,
+ "_subgroup": [
+ { "place": "Kennington", "_count": 1 }
+ ]
+ }))
+ assert_that(results, has_item({
+ "person": "John",
+ "_count": 2,
+ "_group_count": 2,
+ "_subgroup": [
+ { "place": "Kennington", "_count": 1 },
+ { "place": "Kettering", "_count": 1 },
+ ]
+ }))
+
+ def test_grouping_on_non_existent_keys(self):
+ results = self.repo.group("wibble", {})
+
+ assert_that(results, is_([]))
+
+ def test_multi_grouping_on_non_existent_keys(self):
+ result1 = self.repo.multi_group("wibble", "wobble", {})
+ result2 = self.repo.multi_group("wibble", "person", {})
+ result3 = self.repo.multi_group("person", "wibble", {})
+
+ assert_that(result1, is_([]))
+ assert_that(result2, is_([]))
+ assert_that(result3, is_([]))
+
+ def test_multi_grouping_on_empty_collection_returns_empty_list(self):
+ self.mongo_collection.drop()
+ assert_that(list(self.mongo_collection.find({})), is_([]))
+ assert_that(self.repo.multi_group('a', 'b', {}), is_([]))
+
+ def test_multi_grouping_on_same_key_raises_exception(self):
+ self.assertRaises(GroupingError, self.repo.multi_group,
+ "person", "person", {})
+
+ def test_multi_group_is_sorted_by_inner_key(self):
+ results = self.repo.multi_group("person", "_week_start_at", {})
+
+ assert_that(results, has_item(has_entries({
+ "person": "John",
+ "_subgroup": contains(
+ has_entry("_week_start_at", d(2013, 3, 11)),
+ has_entry("_week_start_at", d(2013, 3, 18)),
+ )
+ })))
+
+ def test_sorted_multi_group_query_ascending(self):
+ results = self.repo.multi_group("person", "_week_start_at", {},
+ sort=["_count", "ascending"])
+
+ assert_that(results, contains(
+ has_entry("_count", 1),
+ has_entry("_count", 1),
+ has_entry("_count", 1),
+ has_entry("_count", 2),
+ ))
+
+ def test_sorted_multi_group_query_descending(self):
+ results = self.repo.multi_group("person", "_week_start_at", {},
+ sort=["_count", "descending"])
+
+ assert_that(results, contains(
+ has_entry("_count", 2),
+ has_entry("_count", 1),
+ has_entry("_count", 1),
+ has_entry("_count", 1),
+ ))
+
+ def test_sorted_multi_group_query_ascending_with_limit(self):
+ results = self.repo.multi_group(
+ "person",
+ "_week_start_at",
+ {},
+ sort=["_count", "ascending"],
+ limit=2
+ )
+
+ assert_that(results, contains(
+ has_entry("_count", 1),
+ has_entry("_count", 1),
+ ))
+
+ def test_sorted_multi_group_query_descending_with_limit(self):
+ results = self.repo.multi_group(
+ "person",
+ "_week_start_at",
+ {},
+ sort=["_count", "descending"],
+ limit=2
+ )
+
+ assert_that(results, contains(
+ has_entry("_count", 2),
+ has_entry("_count", 1),
+ ))
+
+
+class TestRepositoryIntegration_Sorting(RepositoryIntegrationTest):
+ def setup_numeric_values(self):
+ self.mongo_collection.save({"value": 6})
+ self.mongo_collection.save({"value": 2})
+ self.mongo_collection.save({"value": 9})
+
+ def setup_playing_cards(self):
+ self.mongo_collection.save({"suite": "clubs"})
+ self.mongo_collection.save({"suite": "hearts"})
+ self.mongo_collection.save({"suite": "clubs"})
+ self.mongo_collection.save({"suite": "diamonds"})
+ self.mongo_collection.save({"suite": "clubs"})
+ self.mongo_collection.save({"suite": "hearts"})
+
+ def test_sorted_query_default_sort_order(self):
+ self.mongo_collection.save({"_timestamp": d(2012, 12, 13)})
+ self.mongo_collection.save({"_timestamp": d(2012, 12, 12)})
+ self.mongo_collection.save({"_timestamp": d(2012, 12, 16)})
+
+ result = self.repo.find({})
+
+ assert_that(list(result), contains(
+ has_entry("_timestamp", d(2012, 12, 12)),
+ has_entry("_timestamp", d(2012, 12, 13)),
+ has_entry("_timestamp", d(2012, 12, 16)),
+ ))
+
+ def test_sorted_query_ascending(self):
+ self.mongo_collection.save({"value": 6})
+ self.mongo_collection.save({"value": 2})
+ self.mongo_collection.save({"value": 9})
+
+ result = self.repo.find({}, sort=["value", "ascending"])
+
+ assert_that(list(result), contains(
+ has_entry('value', 2),
+ has_entry('value', 6),
+ has_entry('value', 9),
+ ))
+
+ def test_sorted_query_descending(self):
+ self.setup_numeric_values()
+
+ result = self.repo.find({}, sort=["value", "descending"])
+
+ assert_that(list(result), contains(
+ has_entry('value', 9),
+ has_entry('value', 6),
+ has_entry('value', 2),
+ ))
+
+ def test_sorted_query_nonsense(self):
+ self.setup_numeric_values()
+
+ self.assertRaises(
+ InvalidSortError,
+ self.repo.find,
+ {}, sort=["value", "coolness"])
+
+ def test_sorted_query_not_enough_args(self):
+ self.setup_numeric_values()
+
+ self.assertRaises(
+ InvalidSortError,
+ self.repo.find,
+ {}, sort=["value"])
+
+ def test_sorted_query_with_alphanumeric(self):
+ self.mongo_collection.save({'val': 'a'})
+ self.mongo_collection.save({'val': 'b'})
+ self.mongo_collection.save({'val': 'c'})
+
+ result = self.repo.find({}, sort=['val', 'descending'])
+ assert_that(list(result), contains(
+ has_entry('val', 'c'),
+ has_entry('val', 'b'),
+ has_entry('val', 'a')
+ ))
+
+ def test_sorted_group_ascending(self):
+ self.setup_playing_cards()
+
+ result = self.repo.group("suite", {}, sort=["suite", "ascending"])
+
+ assert_that(list(result), contains(
+ has_entry("suite", "clubs"),
+ has_entry("suite", "diamonds"),
+ has_entry("suite", "hearts")
+ ))
+
+ def test_sorted_group_descending(self):
+ self.setup_playing_cards()
+
+ result = self.repo.group("suite", {}, sort=["suite", "descending"])
+
+ assert_that(list(result), contains(
+ has_entry("suite", "hearts"),
+ has_entry("suite", "diamonds"),
+ has_entry("suite", "clubs")
+ ))
+
+ def test_sorted_group_nonsense(self):
+ self.setup_playing_cards()
+
+ self.assertRaises(
+ InvalidSortError,
+ self.repo.group,
+ "suite", {}, sort=["suite", "coolness"])
+
+ def test_sorted_group_not_enough_args(self):
+ self.setup_playing_cards()
+
+ self.assertRaises(
+ InvalidSortError,
+ self.repo.group,
+ "suite", {}, sort=["suite"])
+
+ def test_sorted_group_by_count(self):
+ self.setup_playing_cards()
+
+ result = self.repo.group("suite", {}, sort=["_count", "ascending"])
+
+ assert_that(list(result), contains(
+ has_entry("suite", "diamonds"),
+ has_entry("suite", "hearts"),
+ has_entry("suite", "clubs")
+ ))
+
+ def test_sorted_group_by_nonexistent_key(self):
+ self.setup_playing_cards()
+
+ self.assertRaises(
+ InvalidSortError,
+ self.repo.group,
+ "suite", {}, sort=["bleh", "ascending"]
+ )
+
+ def test_sorted_group_by_with_limit(self):
+ self.setup_playing_cards()
+
+ result = self.repo.group(
+ "suite", {}, sort=["_count", "ascending"], limit=1)
+
+ assert_that(list(result), contains(
+ has_entry("suite", "diamonds")
+ ))
+
+ def test_query_with_limit(self):
+ self.mongo_collection.save({"value": 6})
+ self.mongo_collection.save({"value": 2})
+ self.mongo_collection.save({"value": 9})
+
+ result = self.repo.find({}, limit=2)
+
+ assert_that(result.count(with_limit_and_skip=True), is_(2))
+
+ def test_query_with_limit_and_sort(self):
+ self.mongo_collection.save({"value": 6})
+ self.mongo_collection.save({"value": 2})
+ self.mongo_collection.save({"value": 9})
+
+ result = self.repo.find({}, sort=["value", "ascending"], limit=1)
+
+ assert_that(result.count(with_limit_and_skip=True), is_(1))
+ assert_that(list(result)[0], has_entry('value', 2))
View
318 tests/core/integration/test_repository_integration.py
@@ -1,318 +0,0 @@
-import unittest
-import datetime
-from hamcrest import *
-from pymongo import MongoClient
-from backdrop.core.database import Repository, GroupingError
-from tests.support.test_helpers import d
-
-HOST = 'localhost'
-PORT = 27017
-DB_NAME = 'performance_platform_test'
-BUCKET = 'test_repository_integration'
-
-
-class TestRepositoryIntegration(unittest.TestCase):
- def setUp(self):
- self.repo = Repository(MongoClient(HOST, PORT)[DB_NAME][BUCKET])
- self.mongo_collection = MongoClient(HOST, PORT)[DB_NAME][BUCKET]
-
- def tearDown(self):
- self.mongo_collection.drop()
-
- def test_save(self):
- thing_to_save = {'name': 'test_document'}
- another_thing_to_save = {'name': '2nd_test_document'}
-
- self.repo.save(thing_to_save)
- self.repo.save(another_thing_to_save)
-
- results = self.mongo_collection.find()
- assert_that(results, has_item(thing_to_save))
- assert_that(results, has_item(another_thing_to_save))
-
- def test_save_updates_document_with_id(self):
- a_document = { "_id": "event1", "title": "I'm an event"}
- updated_document = {"_id": "event1", "title": "I'm another event"}
-
- self.repo.save(a_document)
- self.repo.save(updated_document)
-
- saved_documents = self.mongo_collection.find()
-
- assert_that( saved_documents, only_contains(updated_document) )
-
- def test_find(self):
- self.mongo_collection.save({"name": "George", "plays": "guitar"})
- self.mongo_collection.save({"name": "John", "plays": "guitar"})
- self.mongo_collection.save({"name": "Paul", "plays": "bass"})
- self.mongo_collection.save({"name": "Ringo", "plays": "drums"})
-
- results = self.repo.find({"plays": "guitar"})
-
- assert_that(results, only_contains(
- has_entries({"name": "George", "plays": "guitar"}),
- has_entries({"name": "John", "plays": "guitar"}),
- ))
-
- def test_group(self):
- self.mongo_collection.save({"name": "George", "plays": "guitar"})
- self.mongo_collection.save({"name": "John", "plays": "guitar"})
- self.mongo_collection.save({"name": "Paul", "plays": "bass"})
- self.mongo_collection.save({"name": "Ringo", "plays": "drums"})
-
- results = self.repo.group("plays", {})
-
- assert_that(results, only_contains(
- has_entries({"plays": "guitar", "_count": 2}),
- has_entries({"plays": "bass", "_count": 1}),
- has_entries({"plays": "drums", "_count": 1})
- ))
-
- def test_group_with_query(self):
- self.mongo_collection.save({"value": '1', "suite": "hearts"})
- self.mongo_collection.save({"value": '1', "suite": "diamonds"})
- self.mongo_collection.save({"value": '1', "suite": "clubs"})
- self.mongo_collection.save({"value": 'K', "suite": "hearts"})
- self.mongo_collection.save({"value": 'K', "suite": "diamonds"})
-
- results = self.repo.group("value", {"suite": "diamonds"})
-
- assert_that(results, only_contains(
- {"value": "1", "_count": 1},
- {"value": "K", "_count": 1}
- ))
-
- def test_key1_is_pulled_to_the_top_of_outer_group(self):
- self.mongo_collection.save({
- "_week_start_at": d(2013, 3, 17, 0, 0, 0),
- "a": 1,
- "b": 2
- })
- self.mongo_collection.save({
- "_week_start_at": d(2013, 3, 24, 0, 0, 0),
- "a": 1,
- "b": 2
- })
-
- result = self.repo.multi_group("_week_start_at", "a", {})
- assert_that(result, has_item(has_entry(
- "_week_start_at", datetime.datetime(2013, 3, 17, 0, 0, 0)
- )))
- assert_that(result, has_item(has_entry(
- "_week_start_at", datetime.datetime(2013, 3, 24, 0, 0, 0)
- )))
-
- def test_should_use_second_key_for_inner_group_name(self):
- self.mongo_collection.save({
- "_week_start_at": d(2013, 3, 17, 0, 0, 0),
- "a": 1,
- "b": 2
- })
- self.mongo_collection.save({
- "_week_start_at": d(2013, 3, 24, 0, 0, 0),
- "a": 1,
- "b": 2
- })
-
- result = self.repo.multi_group("_week_start_at", "a", {})
- assert_that(result, has_item(has_entry(
- "a", {1: {"_count": 1}}
- )))
-
- def test_count_of_outer_elements_should_be_added(self):
- self.mongo_collection.save({
- "_week_start_at": d(2013, 3, 17, 0, 0, 0),
- "a": 1,
- "b": 2
- })
- self.mongo_collection.save({
- "_week_start_at": d(2013, 3, 24, 0, 0, 0),
- "a": 1,
- "b": 2
- })
-
- result = self.repo.multi_group("_week_start_at", "a", {})
- assert_that(result, has_item(has_entry(
- "_count", 1
- )))
-
- def test_grouping_by_multiple_keys(self):
- self.mongo_collection.save({"value": '1',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "diamonds",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "clubs",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "diamonds",
- "hand": 1})
-
- self.mongo_collection.save({"value": '1',
- "suite": "hearts",
- "hand": 2})
- self.mongo_collection.save({"value": '1',
- "suite": "diamonds",
- "hand": 2})
- self.mongo_collection.save({"value": '1',
- "suite": "clubs",
- "hand": 2})
- self.mongo_collection.save({"value": 'K',
- "suite": "hearts",
- "hand": 2})
- self.mongo_collection.save({"value": 'Q',
- "suite": "diamonds",
- "hand": 2})
-
- result = self.repo.multi_group("value", "suite", {})
-
- assert_that(result, has_items(
- {
- "value": '1',
- "_count": 6,
- "_group_count": 3,
- "suite": {
- "hearts": {
- "_count": 2.0
- },
- "clubs": {
- "_count": 2.0
- },
- "diamonds": {
- "_count": 2.0
- }
- }
- },
- {
- "value": 'Q',
- "_count": 1,
- "_group_count": 1,
- "suite": {
- "diamonds": {
- "_count": 1.0
- }
- }
- },
- {
- "value": 'K',
- "_count": 3,
- "_group_count": 2,
- "suite": {
- "hearts": {
- "_count": 2.0
- },
- "diamonds": {
- "_count": 1.0
- }
- }
- }
- ))
-
- def test_grouping_on_non_existent_keys(self):
- self.mongo_collection.save({"value": '1',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "diamonds",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "clubs",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "diamonds",
- "hand": 1})
-
- result1 = self.repo.group('wibble', {})
-
- assert_that(result1, is_([]))
-
- def test_multi_grouping_on_non_existent_keys(self):
- self.mongo_collection.save({"value": '1',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "diamonds",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "clubs",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "diamonds",
- "hand": 1})
-
- self.mongo_collection.save({"value": '1',
- "suite": "hearts",
- "hand": 2})
- self.mongo_collection.save({"value": '1',
- "suite": "diamonds",
- "hand": 2})
- self.mongo_collection.save({"value": '1',
- "suite": "clubs",
- "hand": 2})
- self.mongo_collection.save({"value": 'K',
- "suite": "hearts",
- "hand": 2})
- self.mongo_collection.save({"value": 'Q',
- "suite": "diamonds",
- "hand": 2})
-
- result1 = self.repo.multi_group("wibble", "value", {})
- result2 = self.repo.multi_group("value", "wibble", {})
-
- assert_that(result1, is_([]))
- assert_that(result2, is_([]))
-
- def test_multi_grouping_on_empty_collection_returns_empty_list(self):
- assert_that(list(self.mongo_collection.find({})), is_([]))
- assert_that(self.repo.multi_group('a', 'b', {}), is_([]))
-
- def test_multi_grouping_on_same_key_raises_exception(self):
- self.mongo_collection.save({"value": '1',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "diamonds",
- "hand": 1})
- self.mongo_collection.save({"value": '1',
- "suite": "clubs",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "hearts",
- "hand": 1})
- self.mongo_collection.save({"value": 'K',
- "suite": "diamonds",
- "hand": 1})
-
- self.mongo_collection.save({"value": '1',
- "suite": "hearts",
- "hand": 2})
- self.mongo_collection.save({"value": '1',
- "suite": "diamonds",
- "hand": 2})
- self.mongo_collection.save({"value": '1',
- "suite": "clubs",
- "hand": 2})
- self.mongo_collection.save({"value": 'K',
- "suite": "hearts",
- "hand": 2})
- self.mongo_collection.save({"value": 'Q',
- "suite": "diamonds",
- "hand": 2})
-
- try:
- self.repo.multi_group("suite", "suite", {})
- #fail if exception not raised
- assert_that(False)
- except GroupingError, e:
- assert_that(str(e), is_("Cannot group on two equal keys"))
View
229 tests/core/test_bucket.py
@@ -5,17 +5,7 @@
import pytz
from backdrop.core import bucket
from backdrop.core.records import Record
-
-
-def d(year, month, day, hour, minute, second):
- return datetime.datetime(year=year, month=month, day=day,
- hour=hour, minute=minute, second=second)
-
-
-def d_tz(year, month, day, hour, minute, second):
- return datetime.datetime(year=year, month=month, day=day,
- hour=hour, minute=minute, second=second,
- tzinfo=pytz.UTC)
+from tests.support.test_helpers import d, d_tz
class TestBucket(unittest.TestCase):
@@ -64,7 +54,8 @@ def test_group_by_query(self):
query_result = self.bucket.query(group_by = "name")
- self.mock_repository.group.assert_called_once_with("name", {})
+ self.mock_repository.group.assert_called_once_with(
+ "name", {}, None, None)
assert_that(query_result,
has_item(has_entries({'name': equal_to('Max'),
@@ -73,16 +64,37 @@ def test_group_by_query(self):
has_item(has_entries({'name': equal_to('Gareth'),
'_count': equal_to(2)})))
+ def test_sorted_group_by_query(self):
+ self.bucket.query(
+ group_by="name",
+ sort_by=["name", "ascending"]
+ )
+
+ self.mock_repository.group.assert_called_once_with(
+ "name", {}, ["name", "ascending"], None)
+
+ def test_sorted_group_by_query_with_limit(self):
+ self.bucket.query(
+ group_by="name",
+ sort_by=["name", "ascending"],
+ limit=100
+ )
+
+ self.mock_repository.group.assert_called_once_with(
+ "name", {}, ["name", "ascending"], 100)
+
def test_query_with_start_at(self):
self.bucket.query(start_at = d(2013, 4, 1, 12, 0, 0))
self.mock_repository.find.assert_called_with(
- {"_timestamp": {"$gte": d(2013, 4, 1, 12, 0, 0)}})
+ {"_timestamp": {"$gte": d(2013, 4, 1, 12, 0, 0)}},
+ sort=None, limit=None)
def test_query_with_end_at(self):
self.bucket.query(end_at = d(2013, 4, 1, 12, 0, 0))
self.mock_repository.find.assert_called_with(
- {"_timestamp": {"$lt": d(2013, 4, 1, 12, 0, 0)}})
+ {"_timestamp": {"$lt": d(2013, 4, 1, 12, 0, 0)}},
+ sort=None, limit=None)
def test_query_with_start_at_and__end_at(self):
self.bucket.query(
@@ -95,7 +107,21 @@ def test_query_with_start_at_and__end_at(self):
"$gte": d(2013, 2, 1, 12, 0, 0),
"$lt": d(2013, 3, 1, 12, 0, 0)
}
- })
+ }, sort=None, limit=None)
+
+ def test_query_with_sort(self):
+ self.bucket.query(
+ sort_by=["keyname", "descending"]
+ )
+
+ self.mock_repository.find.assert_called_with(
+ {}, sort=["keyname", "descending"], limit=None
+ )
+
+ def test_query_with_limit(self):
+ self.bucket.query(limit=5)
+
+ self.mock_repository.find.assert_called_with({}, sort=None, limit=5)
def test_week_query(self):
self.mock_repository.group.return_value = [
@@ -106,7 +132,7 @@ def test_week_query(self):
query_result = self.bucket.query(period='week')
self.mock_repository.group.assert_called_once_with(
- "_week_start_at", {})
+ "_week_start_at", {}, limit=None)
assert_that(query_result, has_length(2))
assert_that(query_result, has_item(has_entries({
@@ -120,50 +146,159 @@ def test_week_query(self):
"_count": equal_to(1)
})))
+ def test_week_query_with_limit(self):
+ self.mock_repository.group.return_value = []
+
+ self.bucket.query(period='week', limit=1)
+
+ self.mock_repository.group.assert_called_once_with(
+ "_week_start_at", {}, limit=1)
+
def test_week_and_group_query(self):
self.mock_repository.multi_group.return_value = [
{
- "_week_start_at": d(2013, 1, 7, 0, 0, 0),
- "some_group": {
- "val1": {
+ "some_group": "val1",
+ "_count": 6,
+ "_group_count": 2,
+ "_subgroup": [
+ {
+ "_week_start_at": d(2013, 1, 7, 0, 0, 0),
"_count": 1
},
- "val2": {
- "_count": 2
+ {
+ "_week_start_at": d(2013, 1, 14, 0, 0, 0),
+ "_count": 5
}
- }
+ ]
},
{
- "_week_start_at": d(2013, 1, 14, 0, 0, 0),
- "some_group": {
- "val1": {
- "_count": 5
+ "some_group": "val2",
+ "_count": 8,
+ "_group_count": 2,
+ "_subgroup": [
+ {
+ "_week_start_at": d(2013, 1, 7, 0, 0, 0),
+ "_count": 2
},
- "val2": {
+ {
+ "_week_start_at": d(2013, 1, 14, 0, 0, 0),
"_count": 6
}
- }
+ ]
}
]
query_result = self.bucket.query(period="week", group_by="some_group")
assert_that(query_result, has_length(2))
- assert_that(query_result, has_item(has_entry(
- "some_group", {
- "val1": {"_count": 1},
- "val2": {"_count": 2}
- }
- )))
- assert_that(query_result, has_item(has_entry(
- "some_group", {
- "val1": {"_count": 5},
- "val2": {"_count": 6}
+ assert_that(query_result, has_item(has_entries({
+ "values": has_item({
+ "_start_at": d_tz(2013, 1, 7, 0, 0, 0),
+ "_end_at": d_tz(2013, 1, 14, 0, 0, 0),
+ "_count": 1
+ }),
+ "some_group": "val1"
+ })))
+ assert_that(query_result, has_item(has_entries({
+ "values": has_item({
+ "_start_at": d_tz(2013, 1, 14, 0, 0, 0),
+ "_end_at": d_tz(2013, 1, 21, 0, 0, 0),
+ "_count": 5
+ }),
+ "some_group": "val1"
+ })))
+ assert_that(query_result, has_item(has_entries({
+ "values": has_item({
+ "_start_at": d_tz(2013, 1, 7, 0, 0, 0),
+ "_end_at": d_tz(2013, 1, 14, 0, 0, 0),
+ "_count": 2
+ }),
+ "some_group": "val2"
+ })))
+ assert_that(query_result, has_item(has_entries({
+ "values": has_item({
+ "_start_at": d_tz(2013, 1, 14, 0, 0, 0),
+ "_end_at": d_tz(2013, 1, 21, 0, 0, 0),
+ "_count": 6
+ }),
+ "some_group": "val2"
+ })))
+
+ def test_sorted_week_and_group_query(self):
+ self.mock_repository.multi_group.return_value = [
+ {
+ "some_group": "val1",
+ "_count": 6,
+ "_group_count": 2,
+ "_subgroup": [
+ {
+ "_week_start_at": d(2013, 1, 7, 0, 0, 0),
+ "_count": 1
+ },
+ {
+ "_week_start_at": d(2013, 1, 14, 0, 0, 0),
+ "_count": 5
+ }
+ ]
+ },
+ {
+ "some_group": "val2",
+ "_count": 8,
+ "_group_count": 2,
+ "_subgroup": [
+ {
+ "_week_start_at": d(2013, 1, 7, 0, 0, 0),
+ "_count": 2
+ },
+ {
+ "_week_start_at": d(2013, 1, 14, 0, 0, 0),
+ "_count": 6
+ }
+ ]
+ },
+ ]
+
+ self.bucket.query(
+ period="week",
+ group_by="some_group",
+ sort_by=["_count", "descending"]
+ )
+
+ self.mock_repository.multi_group.assert_called_with(
+ "some_group",
+ "_week_start_at",
+ {},
+ sort=["_count", "descending"],
+ limit=None
+ )
+
+ def test_sorted_week_and_group_query_with_limit(self):
+ self.mock_repository.multi_group.return_value = [
+ {
+ "some_group": "val1",
+ "_count": 6,
+ "_group_count": 2,
+ "_subgroup": [
+ {
+ "_week_start_at": d(2013, 1, 7, 0, 0, 0),
+ "_count": 1
+ },
+ {
+ "_week_start_at": d(2013, 1, 14, 0, 0, 0),
+ "_count": 5
+ }
+ ]
}
- )))
- assert_that(query_result, has_item(has_entry(
- "_start_at", d_tz(2013, 1, 7, 0, 0, 0))))
- assert_that(query_result, has_item(has_entry(
- "_end_at", d_tz(2013, 1, 14, 0, 0, 0))))
- assert_that(query_result, has_item(has_entry(
- "_start_at", d_tz(2013, 1, 14, 0, 0, 0))))
- assert_that(query_result, has_item(has_entry(
- "_end_at", d_tz(2013, 1, 21, 0, 0, 0))))
+ ]
+
+ self.bucket.query(
+ period="week",
+ group_by="some_group",
+ sort_by=["_count", "descending"],
+ limit=1
+ )
+
+ self.mock_repository.multi_group.assert_called_with(
+ "some_group",
+ "_week_start_at",
+ {},
+ sort=["_count", "descending"],
+ limit=1)
View
34 tests/core/test_database.py
@@ -5,14 +5,40 @@
class NestedMergeTestCase(unittest.TestCase):
- def test_nested_merge_merges_dictionaries(self):
- dictionaries = [
+ def setUp(self):
+ self.dictionaries = [
{'a': 1, 'b': 2, 'c': 3},
{'a': 1, 'b': 1, 'c': 3},
+ {'a': 2, 'b': 1, 'c': 3}
]
- output = database.nested_merge(['a', 'b'], dictionaries)
- assert_that(output, is_({1: {2: {'c': 3}, 1: {'c': 3}}}))
+ def test_nested_merge_merges_dictionaries(self):
+ output = database.nested_merge(['a', 'b'], self.dictionaries)
+
+ assert_that(output[0], is_({
+ "a": 1,
+ "_count": 0,
+ "_group_count": 2,
+ "_subgroup": [
+ {"b": 1, "c": 3},
+ {"b": 2, "c": 3},
+ ],
+ }))
+ assert_that(output[1], is_({
+ "a": 2,
+ "_count": 0,
+ "_group_count": 1,
+ "_subgroup": [
+ {"b": 1, "c": 3}
+ ],
+ }))
+
+ def test_nested_merge_squashes_duplicates(self):
+ output = database.nested_merge(['a'], self.dictionaries)
+ assert_that(output, is_([
+ {'a': 1, 'b': 2, 'c': 3},
+ {'a': 2, 'b': 1, 'c': 3}
+ ]))
class TestDatabase(unittest.TestCase):
View
0  tests/core/test_storage.py
No changes.
View
27 tests/read/test_parse_request_args.py
@@ -75,3 +75,30 @@ def test_group_by_is_passed_through_untouched(self):
args = parse_request_args(request_args)
assert_that(args['group_by'], is_('foobar'))
+
+ def test_sort_is_parsed(self):
+ request_args = MultiDict([
+ ("sort_by", "foo:ascending")])
+
+ args = parse_request_args(request_args)
+
+ assert_that(args['sort_by'], is_(["foo", "ascending"]))
+
+ def test_sort_will_use_first_argument_only(self):
+ request_args = MultiDict([
+ ("sort_by", "foo:descending"),
+ ("sort_by", "foo:ascending"),
+ ])
+
+ args = parse_request_args(request_args)
+
+ assert_that(args['sort_by'], is_(["foo", "descending"]))
+
+ def test_limit_is_parsed(self):
+ request_args = MultiDict([
+ ("limit", "123")
+ ])
+
+ args = parse_request_args(request_args)
+
+ assert_that(args['limit'], is_(123))
View
13 tests/read/test_read_api.py
@@ -56,3 +56,16 @@ def test_group_by_with_period_is_executed(self, mock_query):
'/foo?period=week&group_by=stuff'
)
mock_query.assert_called_with(period="week", group_by="stuff")
+
+ @patch('backdrop.core.bucket.Bucket.query')
+ def test_sort_query_is_executed(self, mock_query):
+ mock_query.return_value = None
+ self.app.get(
+ '/foo?sort_by=value:ascending'
+ )
+ mock_query.assert_called_with(sort_by=["value", "ascending"])
+
+ self.app.get(
+ '/foo?sort_by=value:descending'
+ )
+ mock_query.assert_called_with(sort_by=["value", "descending"])
View
57 tests/read/test_validation.py
@@ -47,3 +47,60 @@ def test_accepts_period_with_start_at_and_end_at_present(self):
'end_at': '2010-01-07T00:10:10+00:00',
})
assert_that( validation_result.is_valid, is_(True) )
+
+ def test_rejects_group_by_on_internal_field(self):
+ validation_result = validate_request_args({
+ "group_by": "_internal"
+ })
+ assert_that( validation_result.is_valid, is_(False))
+
+ def test_accepts_ascending_sort_order(self):
+ validation_result = validate_request_args({
+ 'sort_by': 'foo:ascending',
+ })
+ assert_that( validation_result.is_valid, is_(True) )
+
+ def test_accepts_descending_sort_order(self):
+ validation_result = validate_request_args({
+ 'sort_by': 'foo:descending',
+ })
+ assert_that( validation_result.is_valid, is_(True) )
+
+ def test_rejects_unknown_sort_order(self):
+ validation_result = validate_request_args({
+ 'sort_by': 'foo:random',
+ })
+ assert_that( validation_result.is_valid, is_(False) )
+
+ def test_accepts_valid_limit(self):
+ validation_result = validate_request_args({
+ 'limit': '3'
+ })
+ assert_that( validation_result.is_valid, is_(True) )
+
+ def test_rejects_non_integer_limit(self):
+ validation_result = validate_request_args({
+ 'limit': 'not_a_number'
+ })
+ assert_that( validation_result.is_valid, is_(False) )
+
+ def test_rejects_negative_limit(self):
+ validation_result = validate_request_args({
+ 'limit': '-3'
+ })
+ assert_that( validation_result.is_valid, is_(False) )
+
+ def test_rejects_sort_being_provided_with_period_query(self):
+ validation_result = validate_request_args({
+ "sort_by": "foo:ascending",
+ "period": "week"
+ })
+ assert_that( validation_result.is_valid, is_(False) )
+
+ def test_accepts_sort_with_grouped_period_query(self):
+ validation_result = validate_request_args({
+ "sort_by": "foo:ascending",
+ "period": "week",
+ "group_by": "foo"
+ })
+ assert_that( validation_result.is_valid, is_(True) )
View
7 tests/support/test_helpers.py
@@ -51,6 +51,11 @@ def is_error_response():
return IsErrorResponse()
-def d(year, month, day, hour, minute, seconds):
+def d_tz(year, month, day, hour=0, minute=0, seconds=0):
return datetime.datetime(year, month, day, hour, minute, seconds,
tzinfo=pytz.UTC)
+
+
+def d(year, month, day, hour=0, minute=0, second=0):
+ return datetime.datetime(year=year, month=month, day=day,
+ hour=hour, minute=minute, second=second)
Please sign in to comment.
Something went wrong with that request. Please try again.