Skip to content
This repository has been archived by the owner on Mar 24, 2021. It is now read-only.

Commit

Permalink
Use start_at/end_at + duration for relative ranges
Browse files Browse the repository at this point in the history
This replaces the date and delta arguments.

Relative date range queries now look like this: "/foo?start_at=DATETIME&duration=POSITIVE_INT&period=day"
  • Loading branch information
nick-gravgaard committed Jan 29, 2014
1 parent 2ae217d commit ddd704c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 152 deletions.
24 changes: 11 additions & 13 deletions backdrop/read/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ def parse_request_args(request_args):
args['end_at'] = if_present(parse_time_as_utc,
request_args.get('end_at'))

args['delta'] = if_present(int, request_args.get('delta'))

args['date'] = if_present(parse_time_as_utc,
request_args.get('date'))
args['duration'] = if_present(int, request_args.get('duration'))

args['period'] = if_present(parse_period,
request_args.get('period'))
Expand Down Expand Up @@ -61,20 +58,23 @@ def parse_filter_by(filter_by):

_Query = namedtuple(
'_Query',
['start_at', 'end_at', 'date', 'delta', 'period',
['start_at', 'end_at', 'delta', 'period',
'filter_by', 'group_by', 'sort_by', 'limit', 'collect'])


class Query(_Query):
@classmethod
def create(cls,
start_at=None, end_at=None, date=None, delta=None,
start_at=None, end_at=None, duration=None, delta=None,
period=None, filter_by=None, group_by=None,
sort_by=None, limit=None, collect=None):
if delta is not None:
delta = None
if duration is not None:
date = start_at or end_at or now()
delta = duration if start_at else -duration
start_at, end_at = cls.__calculate_start_and_end(period, date,
delta)
return Query(start_at, end_at, date, delta, period,
return Query(start_at, end_at, delta, period,
filter_by or [], group_by, sort_by, limit, collect or [])

@classmethod
Expand All @@ -84,8 +84,6 @@ def parse(cls, request_args):

@staticmethod
def __calculate_start_and_end(period, date, delta):
date = date or now()

duration = period.delta * delta
start_of_period = period.start(date)

Expand All @@ -104,10 +102,10 @@ def __skip_blank_periods(self, results, repository):

def get_shifted_query(self, shift):
"""Return a new Query where the date is shifted by n periods"""
new_date = self.date + (self.period.delta * shift)

args = self._asdict()
args['date'] = new_date

args['start_at'] = args['start_at'] + (self.period.delta * shift)
args['end_at'] = args['end_at'] + (self.period.delta * shift)

return Query.create(**args)

Expand Down
2 changes: 1 addition & 1 deletion backdrop/read/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def first_nonempty(data, is_reversed):

first_nonempty_index = next(
(i for i, d in enumerate(data) if d['_count'] > 0),
None)
0)

if is_reversed:
first_nonempty_index = -first_nonempty_index
Expand Down
61 changes: 33 additions & 28 deletions backdrop/read/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ def __init__(self, request_args):
self.allowed_parameters = set([
'start_at',
'end_at',
'date',
'delta',
'duration',
'period',
'filter_by',
'group_by',
Expand Down Expand Up @@ -72,17 +71,19 @@ def validate(self, request_args, context):

class PeriodQueryValidator(Validator):
def validate(self, request_args, context):
if 'period' in request_args:
if 'delta' not in request_args:
if 'start_at' not in request_args or \
'end_at' not in request_args:
self.add_error("both 'start_at' and 'end_at' are required "
"for a period query")
if 'group_by' not in request_args and 'limit' in request_args:
# When executing a grouped periodic query, the limit is
# applied to the list of groups rather than the time series
# inside them
self.add_error("A grouped period query cannot be limited")
if 'period' not in request_args:
return

if 'duration' not in request_args:
if 'start_at' not in request_args or 'end_at' not in request_args:
self.add_error("Either 'duration' or both 'start_at' and "
"'end_at' are required for a period query")

if 'group_by' not in request_args and 'limit' in request_args:
# When executing a grouped periodic query, the limit is
# applied to the list of groups rather than the time series
# inside them
self.add_error("A grouped period query cannot be limited")


class PositiveIntegerValidator(Validator):
Expand Down Expand Up @@ -269,29 +270,32 @@ def validate(self, request_args, context):
class RelativeTimeValidator(Validator):
def validate(self, request_args, context):

start_at = request_args.get('start_at')
end_at = request_args.get('end_at')
period = request_args.get('period')
date = request_args.get('date')
delta = request_args.get('delta')
duration = request_args.get('duration')

if (request_args.get('start_at') or request_args.get('end_at')) \
and (delta or date):
if start_at and end_at and duration:
self.add_error("Absolute ('start_at' and 'end_at') and relative "
"('delta' and/or 'date') time cannot be requested "
"at the same time")
"('duration') time cannot be requested at the "
"same time")

if start_at and end_at is None and duration is None:
self.add_error("Use of 'start_at' requires 'end_at' or 'duration'")

if date and not delta:
self.add_error("Use of 'date' requires 'delta'")
if end_at and start_at is None and duration is None:
self.add_error("Use of 'end_at' requires 'start_at' or 'duration'")

if delta:
if delta == '0':
self.add_error("'delta' must not be zero")
if duration:
if duration == '0':
self.add_error("'duration' must not be zero")
if not period:
self.add_error("If 'delta' is requested (for relative time), "
"'period' is required")
self.add_error("If 'duration' is requested (for relative "
"time), 'period' is required")
try:
int(delta)
int(duration)
except ValueError:
self.add_error("'delta' is not a valid Integer")
self.add_error("'duration' is not a valid Integer")


def validate_request_args(request_args, raw_queries_allowed=False):
Expand All @@ -310,6 +314,7 @@ def validate_request_args(request_args, raw_queries_allowed=False):
SortByValidator(request_args),
GroupByValidator(request_args),
PositiveIntegerValidator(request_args, param_name='limit'),
PositiveIntegerValidator(request_args, param_name='duration'),
ParamDependencyValidator(request_args, param_name='collect',
depends_on=['group_by', 'period']),
RelativeTimeValidator(request_args),
Expand Down
32 changes: 16 additions & 16 deletions features/read_api/relative_date_ranges.feature
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
@use_read_api_client
Feature: relative date queries for read api

Scenario: querying for periodic data when given one point and a positive delta
Scenario: querying for periodic data from the start_at
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2012-12-12T01:01:02%2B00:00&delta=2&period=day"
when I go to "/foo?start_at=2012-12-12T00:00:00%2B00:00&duration=2&period=day"
then I should get back a status of "200"
and the JSON should have "2" results
and the "1st" result should be "{"_start_at": "2012-12-12T00:00:00+00:00", "_end_at": "2012-12-13T00:00:00+00:00", "_count": 2.0}"
and the "2nd" result should be "{"_start_at": "2012-12-13T00:00:00+00:00", "_end_at": "2012-12-14T00:00:00+00:00", "_count": 1.0}"

Scenario: querying for periodic data when given one point and a negative delta
Scenario: querying for periodic data from the end_at
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2012-12-14T01:01:02%2B00:00&delta=-2&period=day"
when I go to "/foo?end_at=2012-12-14T00:00:00%2B00:00&duration=2&period=day"
then I should get back a status of "200"
and the JSON should have "2" results
and the "1st" result should be "{"_start_at": "2012-12-12T00:00:00+00:00", "_end_at": "2012-12-13T00:00:00+00:00", "_count": 2.0}"
and the "2nd" result should be "{"_start_at": "2012-12-13T00:00:00+00:00", "_end_at": "2012-12-14T00:00:00+00:00", "_count": 1.0}"

Scenario: querying for periodic data when given one point, a positive delta and first results are empty
Scenario: querying for periodic data from the start_at where the first results are empty
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2012-11-01T01:01:02%2B00:00&delta=10&period=week"
when I go to "/foo?start_at=2012-11-05T00:00:00%2B00:00&duration=10&period=week"
then I should get back a status of "200"
and the JSON should have "10" results
and the "1st" result should be "{"_start_at": "2012-12-03T00:00:00+00:00", "_end_at": "2012-12-10T00:00:00+00:00", "_count": 1.0}"
and the "2nd" result should be "{"_start_at": "2012-12-10T00:00:00+00:00", "_end_at": "2012-12-17T00:00:00+00:00", "_count": 4.0}"
and the "3rd" result should be "{"_start_at": "2012-12-17T00:00:00+00:00", "_end_at": "2012-12-24T00:00:00+00:00", "_count": 0.0}"

Scenario: querying for periodic data when given one point, a negative delta and first results are empty
Scenario: querying for periodic data from the end_at where the first results are empty
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2013-02-01T01:01:02%2B00:00&delta=-10&period=week"
when I go to "/foo?end_at=2013-02-04T00:00:00%2B00:00&duration=10&period=week"
then I should get back a status of "200"
and the JSON should have "10" results
and the "10th" result should be "{"_start_at": "2012-12-10T00:00:00+00:00", "_end_at": "2012-12-17T00:00:00+00:00", "_count": 4.0}"
and the "9th" result should be "{"_start_at": "2012-12-03T00:00:00+00:00", "_end_at": "2012-12-10T00:00:00+00:00", "_count": 1.0}"
and the "8th" result should be "{"_start_at": "2012-11-26T00:00:00+00:00", "_end_at": "2012-12-03T00:00:00+00:00", "_count": 0.0}"


Scenario: querying for grouped periodic data when given a positive delta
Scenario: querying for grouped periodic data from the start_at
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2012-12-12T01:01:02%2B00:00&delta=2&period=day&group_by=authority"
when I go to "/foo?start_at=2012-12-12T00:00:00%2B00:00&duration=2&period=day&group_by=authority"
then I should get back a status of "200"
and the JSON should have "2" results
and the "1st" result should have "authority" equaling "Camden"
Expand All @@ -49,9 +49,9 @@ Feature: relative date queries for read api
and the "2nd" result should have "values" with item "{"_start_at": "2012-12-13T00:00:00+00:00", "_end_at": "2012-12-14T00:00:00+00:00", "_count": 1.0}"


Scenario: querying for grouped periodic data when given a negative delta
Scenario: querying for grouped periodic data from the end_at
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2012-12-14T01:01:02%2B00:00&delta=-2&period=day&group_by=authority"
when I go to "/foo?end_at=2012-12-14T00:00:00%2B00:00&duration=2&period=day&group_by=authority"
then I should get back a status of "200"
and the JSON should have "2" results
and the "1st" result should have "authority" equaling "Camden"
Expand All @@ -62,9 +62,9 @@ Feature: relative date queries for read api
and the "2nd" result should have "values" with item "{"_start_at": "2012-12-13T00:00:00+00:00", "_end_at": "2012-12-14T00:00:00+00:00", "_count": 1.0}"


Scenario: querying for grouped periodic data when given a positive delta and first results are empty
Scenario: querying for grouped periodic data from the start_at where the first results are empty
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2012-12-10T01:01:02%2B00:00&delta=4&period=day&group_by=authority"
when I go to "/foo?start_at=2012-12-10T00:00:00%2B00:00&duration=4&period=day&group_by=authority"
then I should get back a status of "200"
and the JSON should have "2" results
and the "1st" result should have "authority" equaling "Camden"
Expand All @@ -73,9 +73,9 @@ Feature: relative date queries for read api
and the "2nd" result should have "_count" equaling the integer "3"


Scenario: querying for grouped periodic data when given a negative delta and first results are empty
Scenario: querying for grouped periodic data from the end_at where the first results are empty
Given "licensing_2.json" is in "foo" bucket
when I go to "/foo?date=2012-12-16T01:01:02%2B00:00&delta=-2&period=day&group_by=authority"
when I go to "/foo?end_at=2012-12-16T00:00:00%2B00:00&duration=2&period=day&group_by=authority"
then I should get back a status of "200"
and the JSON should have "1" results
and the "1st" result should have "authority" equaling "Westminster"
Expand Down
54 changes: 14 additions & 40 deletions tests/read/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,20 @@ def test_build_query_with_start_and_end_at(self):
}))

@freeze_time('2014, 1, 09 00:00:00')
def test_no_date_means_now(self):
def test_no_end_at_means_now(self):
query = Query.create(
period=Day(),
delta=3,
duration=3,
)

assert_that(query.start_at, is_(
assert_that(query.end_at, is_(
datetime(2014, 1, 9, 0, 0, 0, tzinfo=pytz.UTC)))

def test_date_on_boundary_and_positive_delta(self):
def test_start_at_and_duration(self):
query = Query.create(
date=d_tz(2014, 1, 9, 0, 0, 0),
start_at=d_tz(2014, 1, 9, 0, 0, 0),
period=Day(),
delta=3,
duration=3,
)

assert_that(query.start_at, is_(
Expand All @@ -54,11 +54,11 @@ def test_date_on_boundary_and_positive_delta(self):
assert_that(query.end_at, is_(
datetime(2014, 1, 12, 0, 0, 0, tzinfo=pytz.UTC)))

def test_date_on_boundary_and_negative_delta(self):
def test_end_at_and_duration(self):
query = Query.create(
date=d_tz(2014, 1, 11, 0, 0, 0),
end_at=d_tz(2014, 1, 11, 0, 0, 0),
period=Day(),
delta=-3,
duration=3,
)

assert_that(query.start_at, is_(
Expand All @@ -67,37 +67,11 @@ def test_date_on_boundary_and_negative_delta(self):
assert_that(query.end_at, is_(
datetime(2014, 1, 11, 0, 0, 0, tzinfo=pytz.UTC)))

def test_date_off_boundary_and_positive_delta(self):
query = Query.create(
date=d_tz(2014, 1, 9, 1, 2, 3),
period=Day(),
delta=3,
)

assert_that(query.start_at, is_(
datetime(2014, 1, 9, 0, 0, 0, tzinfo=pytz.UTC)))

assert_that(query.end_at, is_(
datetime(2014, 1, 12, 0, 0, 0, tzinfo=pytz.UTC)))

def test_date_off_boundary_and_negative_delta(self):
query = Query.create(
date=d_tz(2014, 1, 11, 23, 58, 57),
period=Day(),
delta=-3,
)

assert_that(query.start_at, is_(
d_tz(2014, 1, 8, 0, 0, 0)))

assert_that(query.end_at, is_(
d_tz(2014, 1, 11, 0, 0, 0)))

def test_shift_query_forward(self):
def test_shift_query_forwards(self):
query = Query.create(
date=d_tz(2014, 1, 9, 0, 0, 0),
start_at=d_tz(2014, 1, 9, 0, 0, 0),
period=Day(),
delta=6,
duration=6,
)

shifted = query.get_shifted_query(5)
Expand All @@ -110,9 +84,9 @@ def test_shift_query_forward(self):

def test_shift_query_backwards(self):
query = Query.create(
date=d_tz(2014, 1, 9, 0, 0, 0),
start_at=d_tz(2014, 1, 9, 0, 0, 0),
period=Day(),
delta=6,
duration=6,
)

shifted = query.get_shifted_query(-5)
Expand Down
Loading

0 comments on commit ddd704c

Please sign in to comment.