Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query datetime parser #2528

Merged
merged 20 commits into from
Jun 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
61b8329
Add a date query precision of ‘hour’
discopatrick Apr 25, 2017
5f2c47e
Test further hour precision intervals
discopatrick Apr 25, 2017
ba324df
Add a date query precision of ‘minute’
discopatrick Apr 25, 2017
b8e1c56
Fix tests
discopatrick Apr 25, 2017
05f0072
Update docstring
discopatrick Apr 26, 2017
6a71504
Allow multiple date formats for each precision
discopatrick Apr 26, 2017
c3771f7
Allow hour precision queries to use space separator
discopatrick Apr 26, 2017
04e2975
Separate date formats onto individual lines
discopatrick Apr 26, 2017
c10eb8f
Keep docstring line <= 79 characters
discopatrick Apr 26, 2017
02bd19f
Allow minute precision queries to use space separator
discopatrick Apr 26, 2017
24890c7
Add a date query precision of ‘second’
discopatrick Apr 26, 2017
1ab913b
Test each valid datetime separator
discopatrick Apr 27, 2017
5a3b74f
Test an invalid datetime separator raises error
discopatrick Apr 27, 2017
6e6dd76
Remove space separator tests from test_x_precision_intervals tests
discopatrick Apr 27, 2017
50a2e37
Keep function names lowercase to pass flake8 tests
discopatrick Apr 27, 2017
1c0b795
Refactor date-finding loop into an inner function
discopatrick May 1, 2017
fbb868e
Merge branch 'master' into query-datetime-parser
discopatrick Jun 1, 2017
e1101d4
Update assertion with correct error name
discopatrick Jun 1, 2017
95eeec9
Add docs for datetime queries
discopatrick Jun 1, 2017
291b287
Add a test for a non-range date query
discopatrick Jun 5, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions beets/dbcore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,12 +533,20 @@ class Period(object):
instants of time during January 2014.
"""

precisions = ('year', 'month', 'day')
date_formats = ('%Y', '%Y-%m', '%Y-%m-%d')
precisions = ('year', 'month', 'day', 'hour', 'minute', 'second')
date_formats = (
('%Y',), # year
('%Y-%m',), # month
('%Y-%m-%d',), # day
('%Y-%m-%dT%H', '%Y-%m-%d %H'), # hour
('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M'), # minute
('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S') # second
)

def __init__(self, date, precision):
"""Create a period with the given date (a `datetime` object) and
precision (a string, one of "year", "month", or "day").
precision (a string, one of "year", "month", "day", "hour", "minute",
or "second").
"""
if precision not in Period.precisions:
raise ValueError(u'Invalid precision {0}'.format(precision))
Expand All @@ -551,16 +559,21 @@ def parse(cls, string):
string is empty, or raise an InvalidQueryArgumentValueError if
the string could not be parsed to a date.
"""

def find_date_and_format(string):
for ord, format in enumerate(cls.date_formats):
for format_option in format:
try:
date = datetime.strptime(string, format_option)
return date, ord
except ValueError:
# Parsing failed.
pass
return (None, None)

if not string:
return None
date = None
for ordinal, date_format in enumerate(cls.date_formats):
try:
date = datetime.strptime(string, date_format)
break
except ValueError:
# Parsing failed.
pass
date, ordinal = find_date_and_format(string)
if date is None:
raise InvalidQueryArgumentValueError(string,
'a valid datetime string')
Expand All @@ -582,6 +595,12 @@ def open_right_endpoint(self):
return date.replace(year=date.year + 1, month=1)
elif 'day' == precision:
return date + timedelta(days=1)
elif 'hour' == precision:
return date + timedelta(hours=1)
elif 'minute' == precision:
return date + timedelta(minutes=1)
elif 'second' == precision:
return date + timedelta(seconds=1)
else:
raise ValueError(u'unhandled precision {0}'.format(precision))

Expand Down
27 changes: 27 additions & 0 deletions docs/reference/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,33 @@ Find all items with a file modification time between 2008-12-01 and

$ beet ls 'mtime:2008-12-01..2008-12-02'

You can also add an optional time value to date queries, specifying hours,
minutes, and seconds.

Times are separated from dates by a space, an uppercase 'T' or a lowercase
't', for example: ``2008-12-01T23:59:59``. If you specify a time, then the
date must contain a year, month, and day. The minutes and seconds are
optional.

Here is an example that finds all items added on 2008-12-01 at or after 22:00
but before 23:00::

$ beet ls 'added:2008-12-01T22'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the "but before 23:00" part?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you're asking why there isn't a .. in the query, followed by the right hand part of the range?

This is because by specifying the hour of 22, results from anywhere within that hour are returned. The right hand part of the range is implied.

This is the same way that date queries already work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok but the query returns items added between 23:00 and 0:00

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, confusion from my part with queries with the .. at the end.
Cool you added the case in the test though !

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem! It might be worth having a non-range test for all precisions?


Find all items added on or after 2008-12-01 22:45::

$ beet ls 'added:2008-12-01T22:45..'

Find all items added on 2008-12-01, at or after 22:45:20 but before 22:45:41::

$ beet ls 'added:2008-12-01T22:45:20..2008-12-01T22:45:40'

Examples of each time format::

$ beet ls 'added:2008-12-01T22:45:20'
$ beet ls 'added:2008-12-01t22:45:20'
$ beet ls 'added:2008-12-01 22:45:20'

.. _not_query:

Query Term Negation
Expand Down
64 changes: 64 additions & 0 deletions test/test_datequery.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,51 @@ def test_month_precision_intervals(self):
self.assertExcludes('1999-12..2000-02', '1999-11-30T23:59:59')
self.assertExcludes('1999-12..2000-02', '2000-03-01T00:00:00')

def test_hour_precision_intervals(self):
# test with 'T' separator
self.assertExcludes('2000-01-01T12..2000-01-01T13',
'2000-01-01T11:59:59')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T12:00:00')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T12:30:00')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T13:30:00')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T13:59:59')
self.assertExcludes('2000-01-01T12..2000-01-01T13',
'2000-01-01T14:00:00')
self.assertExcludes('2000-01-01T12..2000-01-01T13',
'2000-01-01T14:30:00')

# test non-range query
self.assertContains('2008-12-01T22',
'2008-12-01T22:30:00')
self.assertExcludes('2008-12-01T22',
'2008-12-01T23:30:00')

def test_minute_precision_intervals(self):
self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:29:59')
self.assertContains('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:30:00')
self.assertContains('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:30:30')
self.assertContains('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:31:59')
self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:32:00')

def test_second_precision_intervals(self):
self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:49')
self.assertContains('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:50')
self.assertContains('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:55')
self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:56')

def test_unbounded_endpoints(self):
self.assertContains('..', date=datetime.max)
self.assertContains('..', date=datetime.min)
Expand Down Expand Up @@ -140,6 +185,25 @@ def test_invalid_date_query(self):
with self.assertRaises(InvalidQueryArgumentValueError):
DateQuery('added', q)

def test_datetime_uppercase_t_separator(self):
date_query = DateQuery('added', '2000-01-01T12')
self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12))
self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13))

def test_datetime_lowercase_t_separator(self):
date_query = DateQuery('added', '2000-01-01t12')
self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12))
self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13))

def test_datetime_space_separator(self):
date_query = DateQuery('added', '2000-01-01 12')
self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12))
self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13))

def test_datetime_invalid_separator(self):
with self.assertRaises(InvalidQueryArgumentValueError):
DateQuery('added', '2000-01-01x12')


def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
Expand Down