From 47b0b54fa1a6d76c5035f82b4cde9903f4381d7c Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 19:15:21 +0100 Subject: [PATCH 01/15] trying relative dates --- beets/dbcore/query.py | 51 +++++++++++++++++++++++++++++++------------ tox.ini | 1 + 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 470ca2ac6..8d778074f 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -21,6 +21,7 @@ import re from operator import mul from beets import util from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta import unicodedata from functools import reduce import six @@ -533,8 +534,9 @@ class Period(object): instants of time during January 2014. """ - precisions = ('year', 'month', 'day') + precisions = ('year', 'month', 'day', 'relative') date_formats = ('%Y', '%Y-%m', '%Y-%m-%d') + relative = ('y', 'm', 'w', 'd') def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and @@ -550,19 +552,38 @@ class Period(object): """Parse a date and return a `Period` object or `None` if the string is empty. """ - if not string: - return None - ordinal = string.count('-') - if ordinal >= len(cls.date_formats): - # Too many components. - return None - date_format = cls.date_formats[ordinal] - try: - date = datetime.strptime(string, date_format) - except ValueError: - # Parsing failed. - return None - precision = cls.precisions[ordinal] + if re.match('@([+|-]?)(\d+)([y|m|w|d])', string) is not None: + sign = re.match('@([+|-]?)(\d+)([y|m|w|d])', string).group(1) + quantity = re.match('@([+|-]?)(\d+)([y|m|w|d])', string).group(2) + timespan = re.match('@([+|-]?)(\d+)([y|m|w|d])', string).group(3) + if sign == '-': + m = -1 + else: + m = 1 + if timespan == 'y': + date = datetime.now() + m*relativedelta(years=int(quantity)) + elif timespan == 'm': + date = datetime.now() + m*relativedelta(months=int(quantity)) + elif timespan == 'w': + date = datetime.now() + m*relativedelta(weeks=int(quantity)) + elif timespan == 'd': + date = datetime.now() + m*relativedelta(days=int(quantity)) + + precision = 'relative' + else: + if not string: + return None + ordinal = string.count('-') + if ordinal >= len(cls.date_formats): + # Too many components. + return None + date_format = cls.date_formats[ordinal] + try: + date = datetime.strptime(string, date_format) + except ValueError: + # Parsing failed. + return None + precision = cls.precisions[ordinal] return cls(date, precision) def open_right_endpoint(self): @@ -571,6 +592,8 @@ class Period(object): """ precision = self.precision date = self.date + if 'relative' == self.precision: + return date if 'year' == self.precision: return date.replace(year=date.year + 1, month=1) elif 'month' == precision: diff --git a/tox.ini b/tox.ini index 43bff8014..477e3c874 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,7 @@ deps = python-mpd2 coverage discogs-client + dateutils [_flake8] deps = From 5624e24622b2554f9cce1d002868b25e80122e8e Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 19:19:01 +0100 Subject: [PATCH 02/15] trying relative dates --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1019a74a7..ec582105f 100755 --- a/setup.py +++ b/setup.py @@ -106,7 +106,8 @@ setup( 'pyxdg', 'pathlib', 'python-mpd2', - 'discogs-client' + 'discogs-client', + 'dateutils' ], # Plugin (optional) dependencies: From 03dfaf97ee0ca7778db4799a07e7aa1662f6447d Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 20:15:08 +0100 Subject: [PATCH 03/15] wrong dateutil package --- setup.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ec582105f..cd3911134 100755 --- a/setup.py +++ b/setup.py @@ -107,7 +107,7 @@ setup( 'pathlib', 'python-mpd2', 'discogs-client', - 'dateutils' + 'python-dateutil' ], # Plugin (optional) dependencies: diff --git a/tox.ini b/tox.ini index 477e3c874..8ba69faff 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ deps = python-mpd2 coverage discogs-client - dateutils + python-dateutil [_flake8] deps = From bee758a19643998b1841bcfe3440e94fdd053a13 Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 20:15:25 +0100 Subject: [PATCH 04/15] some flake8 spacing --- beets/dbcore/query.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 8d778074f..dcbecd595 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -561,13 +561,13 @@ class Period(object): else: m = 1 if timespan == 'y': - date = datetime.now() + m*relativedelta(years=int(quantity)) + date = datetime.now() + m * relativedelta(years=int(quantity)) elif timespan == 'm': - date = datetime.now() + m*relativedelta(months=int(quantity)) + date = datetime.now() + m * relativedelta(months=int(quantity)) elif timespan == 'w': - date = datetime.now() + m*relativedelta(weeks=int(quantity)) + date = datetime.now() + m * relativedelta(weeks=int(quantity)) elif timespan == 'd': - date = datetime.now() + m*relativedelta(days=int(quantity)) + date = datetime.now() + m * relativedelta(days=int(quantity)) precision = 'relative' else: From 9bc75b042f3728f53a7c8598d6f04838071d0506 Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 20:27:28 +0100 Subject: [PATCH 05/15] need dateutil in install ? --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cd3911134..672402603 100755 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ setup( 'musicbrainzngs>=0.4', 'pyyaml', 'jellyfish', + 'python-dateutil', ] + (['colorama'] if (sys.platform == 'win32') else []) + (['enum34>=1.0.4'] if sys.version_info < (3, 4, 0) else []), @@ -107,7 +108,7 @@ setup( 'pathlib', 'python-mpd2', 'discogs-client', - 'python-dateutil' + 'python-dateutil', ], # Plugin (optional) dependencies: From 868746bb5110b09f16162a4f47b8f6e473b4403b Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 21:23:03 +0100 Subject: [PATCH 06/15] tests --- test/test_datequery.py | 44 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index 1e1625db2..4b01edd2c 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -17,6 +17,7 @@ """ from __future__ import division, absolute_import, print_function +from dateutil.relativedelta import relativedelta from test import _common from datetime import datetime import unittest @@ -28,6 +29,10 @@ def _date(string): return datetime.strptime(string, '%Y-%m-%dT%H:%M:%S') +def _datepattern(datetimedate): + return datetimedate.strftime('%Y-%m-%dT%H:%M:%S') + + class DateIntervalTest(unittest.TestCase): def test_year_precision_intervals(self): self.assertContains('2000..2001', '2000-01-01T00:00:00') @@ -43,6 +48,9 @@ class DateIntervalTest(unittest.TestCase): self.assertContains('..2001', '2001-12-31T23:59:59') self.assertExcludes('..2001', '2002-01-01T00:00:00') + self.assertContains('@-1d..@1d', _datepattern(datetime.now())) + self.assertExcludes('@-2d..@-1d', _datepattern(datetime.now())) + def test_day_precision_intervals(self): self.assertContains('2000-06-20..2000-06-20', '2000-06-20T00:00:00') self.assertContains('2000-06-20..2000-06-20', '2000-06-20T10:20:30') @@ -62,7 +70,8 @@ class DateIntervalTest(unittest.TestCase): self.assertContains('..', date=datetime.min) self.assertContains('..', '1000-01-01T00:00:00') - def assertContains(self, interval_pattern, date_pattern=None, date=None): # noqa + def assertContains(self, interval_pattern, date_pattern=None, + date=None): # noqa if date is None: date = _date(date_pattern) (start, end) = _parse_periods(interval_pattern) @@ -114,6 +123,39 @@ class DateQueryTest(_common.LibTestCase): matched = self.lib.items(query) self.assertEqual(len(matched), 0) +class DateQueryTestRelative(_common.LibTestCase): + def setUp(self): + super(DateQueryTestRelative, self).setUp() + self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M')) + self.i.store() + + def test_single_month_match_fast(self): + query = DateQuery('added', datetime.now().strftime('%Y-%m')) + matched = self.lib.items(query) + self.assertEqual(len(matched), 1) + + def test_single_month_nonmatch_fast(self): + query = DateQuery('added', (datetime.now()+relativedelta(months=1)).strftime('%Y-%m')) + matched = self.lib.items(query) + self.assertEqual(len(matched), 0) + + def test_single_month_match_slow(self): + query = DateQuery('added', datetime.now().strftime('%Y-%m')) + self.assertTrue(query.match(self.i)) + + def test_single_month_nonmatch_slow(self): + query = DateQuery('added', (datetime.now()+relativedelta(months=1)).strftime('%Y-%m')) + self.assertFalse(query.match(self.i)) + + def test_single_day_match_fast(self): + query = DateQuery('added', datetime.now().strftime('%Y-%m-%d')) + matched = self.lib.items(query) + self.assertEqual(len(matched), 1) + + def test_single_day_nonmatch_fast(self): + query = DateQuery('added', (datetime.now()+ relativedelta(days=1)).strftime('%Y-%m-%d')) + matched = self.lib.items(query) + self.assertEqual(len(matched), 0) class DateQueryConstructTest(unittest.TestCase): def test_long_numbers(self): From 8d054f3656aa74762d9473dec67cdc39ea9d471c Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 21:40:26 +0100 Subject: [PATCH 07/15] tests flake8 issues added some doc --- docs/reference/query.rst | 15 +++++++++++++++ test/test_datequery.py | 14 +++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 2f3366d4c..c2a5cdf0a 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -162,6 +162,20 @@ Dates are written separated by hyphens, like ``year-month-day``, but the month and day are optional. If you leave out the day, for example, you will get matches for the whole month. +You can also use relative dates to the current time. +Relative dates begin by the ``@`` character, followed by an optional ``+`` or +``-`` sign that will increment or decrement now, followed by the time quantity +that will be represented as an integer followed by either ``d`` for days, ``w`` +for weeks, ``m`` for months and finally ``y`` for year. + +Here is an example that finds all the albums added between now and last week:: + + $ beet ls -a 'added:@-1w..@0d' + +Find all items added in a 2 weeks period 4 weeks ago:: + + $ beet ls -a 'added:@-6w..@-2w' + Date *intervals*, like the numeric intervals described above, are separated by two dots (``..``). You can specify a start, an end, or both. @@ -186,6 +200,7 @@ Find all items with a file modification time between 2008-12-01 and $ beet ls 'mtime:2008-12-01..2008-12-02' + .. _not_query: Query Term Negation diff --git a/test/test_datequery.py b/test/test_datequery.py index 4b01edd2c..e2d1d23a3 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -123,6 +123,7 @@ class DateQueryTest(_common.LibTestCase): matched = self.lib.items(query) self.assertEqual(len(matched), 0) + class DateQueryTestRelative(_common.LibTestCase): def setUp(self): super(DateQueryTestRelative, self).setUp() @@ -135,7 +136,9 @@ class DateQueryTestRelative(_common.LibTestCase): self.assertEqual(len(matched), 1) def test_single_month_nonmatch_fast(self): - query = DateQuery('added', (datetime.now()+relativedelta(months=1)).strftime('%Y-%m')) + query = DateQuery('added', + (datetime.now() + relativedelta(months=1)).strftime( + '%Y-%m')) matched = self.lib.items(query) self.assertEqual(len(matched), 0) @@ -144,7 +147,9 @@ class DateQueryTestRelative(_common.LibTestCase): self.assertTrue(query.match(self.i)) def test_single_month_nonmatch_slow(self): - query = DateQuery('added', (datetime.now()+relativedelta(months=1)).strftime('%Y-%m')) + query = DateQuery('added', + (datetime.now() + relativedelta(months=1)).strftime( + '%Y-%m')) self.assertFalse(query.match(self.i)) def test_single_day_match_fast(self): @@ -153,10 +158,13 @@ class DateQueryTestRelative(_common.LibTestCase): self.assertEqual(len(matched), 1) def test_single_day_nonmatch_fast(self): - query = DateQuery('added', (datetime.now()+ relativedelta(days=1)).strftime('%Y-%m-%d')) + query = DateQuery('added', + (datetime.now() + relativedelta(days=1)).strftime( + '%Y-%m-%d')) matched = self.lib.items(query) self.assertEqual(len(matched), 0) + class DateQueryConstructTest(unittest.TestCase): def test_long_numbers(self): DateQuery('added', '1409830085..1412422089') From 2b89b90ab6683922687d179147aafe2565d331d4 Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 21:47:01 +0100 Subject: [PATCH 08/15] tests flake8 fixed I think finally --- test/test_datequery.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index e2d1d23a3..95e617084 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -70,8 +70,7 @@ class DateIntervalTest(unittest.TestCase): self.assertContains('..', date=datetime.min) self.assertContains('..', '1000-01-01T00:00:00') - def assertContains(self, interval_pattern, date_pattern=None, - date=None): # noqa + def assertContains(self, interval_pattern, date_pattern=None, date=None): # noqa if date is None: date = _date(date_pattern) (start, end) = _parse_periods(interval_pattern) From 3e76c219fb6a6d893f45efcb49f574970e65e073 Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 22:39:37 +0100 Subject: [PATCH 09/15] without dateutil --- beets/dbcore/query.py | 10 ++++------ setup.py | 2 -- test/test_datequery.py | 18 +++++++----------- tox.ini | 1 - 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index dcbecd595..0b0cfa6f7 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -21,7 +21,6 @@ import re from operator import mul from beets import util from datetime import datetime, timedelta -from dateutil.relativedelta import relativedelta import unicodedata from functools import reduce import six @@ -561,14 +560,13 @@ class Period(object): else: m = 1 if timespan == 'y': - date = datetime.now() + m * relativedelta(years=int(quantity)) + date = datetime.now() + m * timedelta(days=int(quantity) * 365) elif timespan == 'm': - date = datetime.now() + m * relativedelta(months=int(quantity)) + date = datetime.now() + m * timedelta(days=int(quantity) * 30) elif timespan == 'w': - date = datetime.now() + m * relativedelta(weeks=int(quantity)) + date = datetime.now() + timedelta(days=int(quantity) * 7) elif timespan == 'd': - date = datetime.now() + m * relativedelta(days=int(quantity)) - + date = datetime.now() + m * timedelta(days=int(quantity)) precision = 'relative' else: if not string: diff --git a/setup.py b/setup.py index 672402603..f6e686d35 100755 --- a/setup.py +++ b/setup.py @@ -93,7 +93,6 @@ setup( 'musicbrainzngs>=0.4', 'pyyaml', 'jellyfish', - 'python-dateutil', ] + (['colorama'] if (sys.platform == 'win32') else []) + (['enum34>=1.0.4'] if sys.version_info < (3, 4, 0) else []), @@ -108,7 +107,6 @@ setup( 'pathlib', 'python-mpd2', 'discogs-client', - 'python-dateutil', ], # Plugin (optional) dependencies: diff --git a/test/test_datequery.py b/test/test_datequery.py index 95e617084..cefc03b61 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -17,9 +17,8 @@ """ from __future__ import division, absolute_import, print_function -from dateutil.relativedelta import relativedelta from test import _common -from datetime import datetime +from datetime import datetime, timedelta import unittest import time from beets.dbcore.query import _parse_periods, DateInterval, DateQuery @@ -135,9 +134,8 @@ class DateQueryTestRelative(_common.LibTestCase): self.assertEqual(len(matched), 1) def test_single_month_nonmatch_fast(self): - query = DateQuery('added', - (datetime.now() + relativedelta(months=1)).strftime( - '%Y-%m')) + query = DateQuery('added', (datetime.now() + timedelta(days=30)) + .strftime('%Y-%m')) matched = self.lib.items(query) self.assertEqual(len(matched), 0) @@ -146,9 +144,8 @@ class DateQueryTestRelative(_common.LibTestCase): self.assertTrue(query.match(self.i)) def test_single_month_nonmatch_slow(self): - query = DateQuery('added', - (datetime.now() + relativedelta(months=1)).strftime( - '%Y-%m')) + query = DateQuery('added', (datetime.now() + timedelta(days=30)) + .strftime('%Y-%m')) self.assertFalse(query.match(self.i)) def test_single_day_match_fast(self): @@ -157,9 +154,8 @@ class DateQueryTestRelative(_common.LibTestCase): self.assertEqual(len(matched), 1) def test_single_day_nonmatch_fast(self): - query = DateQuery('added', - (datetime.now() + relativedelta(days=1)).strftime( - '%Y-%m-%d')) + query = DateQuery('added', (datetime.now() + timedelta(days=1)) + .strftime('%Y-%m-%d')) matched = self.lib.items(query) self.assertEqual(len(matched), 0) diff --git a/tox.ini b/tox.ini index 8ba69faff..43bff8014 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,6 @@ deps = python-mpd2 coverage discogs-client - python-dateutil [_flake8] deps = From af679de8ec1b15035926c1ba69884994046ef20f Mon Sep 17 00:00:00 2001 From: euri10 Date: Sun, 29 Jan 2017 03:51:24 +0100 Subject: [PATCH 10/15] using a pattern may avoid copy-paste error when used 3 times after fixed an error with the weeks that didn't use the sign correctly added more tests, this is where py.test fixtures would shine --- beets/dbcore/query.py | 11 ++++++----- test/test_datequery.py | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 0b0cfa6f7..76679b021 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -551,10 +551,11 @@ class Period(object): """Parse a date and return a `Period` object or `None` if the string is empty. """ - if re.match('@([+|-]?)(\d+)([y|m|w|d])', string) is not None: - sign = re.match('@([+|-]?)(\d+)([y|m|w|d])', string).group(1) - quantity = re.match('@([+|-]?)(\d+)([y|m|w|d])', string).group(2) - timespan = re.match('@([+|-]?)(\d+)([y|m|w|d])', string).group(3) + pattern_dq = '@([+|-]?)(\d+)([y|m|w|d])' + if re.match(pattern_dq, string) is not None: + sign = re.match(pattern_dq, string).group(1) + quantity = re.match(pattern_dq, string).group(2) + timespan = re.match(pattern_dq, string).group(3) if sign == '-': m = -1 else: @@ -564,7 +565,7 @@ class Period(object): elif timespan == 'm': date = datetime.now() + m * timedelta(days=int(quantity) * 30) elif timespan == 'w': - date = datetime.now() + timedelta(days=int(quantity) * 7) + date = datetime.now() + m * timedelta(days=int(quantity) * 7) elif timespan == 'd': date = datetime.now() + m * timedelta(days=int(quantity)) precision = 'relative' diff --git a/test/test_datequery.py b/test/test_datequery.py index cefc03b61..e74c9a0db 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -160,6 +160,49 @@ class DateQueryTestRelative(_common.LibTestCase): self.assertEqual(len(matched), 0) +class DateQueryTestRelativeMore(_common.LibTestCase): + def setUp(self): + super(DateQueryTestRelativeMore, self).setUp() + self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M')) + self.i.store() + + def test_relative(self): + for timespan in ['d', 'w', 'm', 'y']: + query = DateQuery('added', '@-4'+timespan+'..@+4'+timespan) + matched = self.lib.items(query) + self.assertEqual(len(matched), 1) + + def test_relative_fail(self): + for timespan in ['d', 'w', 'm', 'y']: + query = DateQuery('added', '@-2'+timespan+'..@-1'+timespan) + matched = self.lib.items(query) + self.assertEqual(len(matched), 0) + + def test_start_relative(self): + for timespan in ['d', 'w', 'm', 'y']: + query = DateQuery('added', '@-4' + timespan + '..') + matched = self.lib.items(query) + self.assertEqual(len(matched), 1) + + def test_start_relative_fail(self): + for timespan in ['d', 'w', 'm', 'y']: + query = DateQuery('added', '@4' + timespan + '..') + matched = self.lib.items(query) + self.assertEqual(len(matched), 0) + + def test_end_relative(self): + for timespan in ['d', 'w', 'm', 'y']: + query = DateQuery('added', '..@+4' + timespan) + matched = self.lib.items(query) + self.assertEqual(len(matched), 1) + + def test_end_relative_fail(self): + for timespan in ['d', 'w', 'm', 'y']: + query = DateQuery('added', '..@-4' + timespan) + matched = self.lib.items(query) + self.assertEqual(len(matched), 0) + + class DateQueryConstructTest(unittest.TestCase): def test_long_numbers(self): DateQuery('added', '1409830085..1412422089') From d48d1f8e3cf10e158f2398040caec2d778fb73b4 Mon Sep 17 00:00:00 2001 From: euri10 Date: Sun, 29 Jan 2017 03:54:10 +0100 Subject: [PATCH 11/15] fixed E226 flake8 --- test/test_datequery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index e74c9a0db..3e546b195 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -168,13 +168,13 @@ class DateQueryTestRelativeMore(_common.LibTestCase): def test_relative(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '@-4'+timespan+'..@+4'+timespan) + query = DateQuery('added', '@-4' + timespan + '..@+4'+ timespan) matched = self.lib.items(query) self.assertEqual(len(matched), 1) def test_relative_fail(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '@-2'+timespan+'..@-1'+timespan) + query = DateQuery('added', '@-2' + timespan + '..@-1' + timespan) matched = self.lib.items(query) self.assertEqual(len(matched), 0) From e4a7d37a6d55a9a8cf06423330635b128857fab9 Mon Sep 17 00:00:00 2001 From: euri10 Date: Sun, 29 Jan 2017 11:49:22 +0100 Subject: [PATCH 12/15] implementing changes asked corrected rst fixed flake8 in test --- beets/dbcore/query.py | 42 ++++++++++++++++++++++++---------------- docs/reference/query.rst | 2 +- test/test_datequery.py | 2 +- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 76679b021..337e3074c 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -535,7 +535,7 @@ class Period(object): precisions = ('year', 'month', 'day', 'relative') date_formats = ('%Y', '%Y-%m', '%Y-%m-%d') - relative = ('y', 'm', 'w', 'd') + relative = {'y': 365, 'm': 30, 'w': 7, 'd': 1} def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and @@ -550,24 +550,30 @@ class Period(object): def parse(cls, string): """Parse a date and return a `Period` object or `None` if the string is empty. + Depending on the string, the date can be absolute or + relative. + An absolute date has to be like one of the date_formats '%Y' or '%Y-%m' + or '%Y-%m-%d' + A relative date begins by '@ 'and has to follow the pattern_dq format + '@([+|-]?)(\d+)([y|m|w|d])' + - '@' indicates it's a date relative to now() + - the optional '+' or '-' sign, which defaults to '+' will increment or + decrement now() by a certain quantity + - that quantity can be expressed in days, weeks, months or years + respectively 'd', 'w', 'm', 'y' + Please note that this relative calculation is rather approximate as it + makes the assumption of 30 days per month and 365 days per year """ pattern_dq = '@([+|-]?)(\d+)([y|m|w|d])' - if re.match(pattern_dq, string) is not None: - sign = re.match(pattern_dq, string).group(1) - quantity = re.match(pattern_dq, string).group(2) - timespan = re.match(pattern_dq, string).group(3) - if sign == '-': - m = -1 - else: - m = 1 - if timespan == 'y': - date = datetime.now() + m * timedelta(days=int(quantity) * 365) - elif timespan == 'm': - date = datetime.now() + m * timedelta(days=int(quantity) * 30) - elif timespan == 'w': - date = datetime.now() + m * timedelta(days=int(quantity) * 7) - elif timespan == 'd': - date = datetime.now() + m * timedelta(days=int(quantity)) + match_dq = re.match(pattern_dq, string) + if match_dq is not None: + sign = match_dq.group(1) + quantity = match_dq.group(2) + timespan = match_dq.group(3) + multiplier = -1 if sign == '-' else 1 + days = cls.relative[timespan] + date = datetime.now() + multiplier * timedelta( + days=int(quantity) * days) precision = 'relative' else: if not string: @@ -648,6 +654,7 @@ class DateQuery(FieldQuery): The value of a date field can be matched against a date interval by using an ellipsis interval syntax similar to that of NumericQuery. """ + def __init__(self, field, pattern, fast=True): super(DateQuery, self).__init__(field, pattern, fast) start, end = _parse_periods(pattern) @@ -691,6 +698,7 @@ class DurationQuery(NumericQuery): Raises InvalidQueryError when the pattern does not represent an int, float or M:SS time interval. """ + def _convert(self, s): """Convert a M:SS or numeric string to a float. diff --git a/docs/reference/query.rst b/docs/reference/query.rst index c2a5cdf0a..da97b0d87 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -170,7 +170,7 @@ for weeks, ``m`` for months and finally ``y`` for year. Here is an example that finds all the albums added between now and last week:: - $ beet ls -a 'added:@-1w..@0d' + $ beet ls -a 'added:@-1w..' Find all items added in a 2 weeks period 4 weeks ago:: diff --git a/test/test_datequery.py b/test/test_datequery.py index 3e546b195..d0c18e229 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -168,7 +168,7 @@ class DateQueryTestRelativeMore(_common.LibTestCase): def test_relative(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '@-4' + timespan + '..@+4'+ timespan) + query = DateQuery('added', '@-4' + timespan + '..@+4' + timespan) matched = self.lib.items(query) self.assertEqual(len(matched), 1) From c9177f2b5672642c19542c4a8c7c8c42baf4d221 Mon Sep 17 00:00:00 2001 From: euri10 Date: Sun, 29 Jan 2017 14:38:25 +0100 Subject: [PATCH 13/15] removed unrelated PR changes corrected docs with correct example added relative date usage to it --- docs/reference/query.rst | 17 +++++++++++------ setup.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index da97b0d87..6c0dae5ae 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -163,10 +163,16 @@ and day are optional. If you leave out the day, for example, you will get matches for the whole month. You can also use relative dates to the current time. -Relative dates begin by the ``@`` character, followed by an optional ``+`` or -``-`` sign that will increment or decrement now, followed by the time quantity -that will be represented as an integer followed by either ``d`` for days, ``w`` -for weeks, ``m`` for months and finally ``y`` for year. +A relative date begins by ``@`` and has to follow the pattern_dq format +``@([+|-]?)(\d+)([y|m|w|d])`` +- ``@`` indicates it's a date relative to now() +- the optional ``+`` or ``-`` sign, which defaults to ``+`` will increment or +decrement now() by a certain quantity +- that quantity can be expressed in days, weeks, months or years respectively +'d', 'w', 'm', 'y' + +Please note that this relative calculation is rather approximate as it makes +the assumption of 30 days per month and 365 days per year. Here is an example that finds all the albums added between now and last week:: @@ -174,7 +180,7 @@ Here is an example that finds all the albums added between now and last week:: Find all items added in a 2 weeks period 4 weeks ago:: - $ beet ls -a 'added:@-6w..@-2w' + $ beet ls -a 'added:@-6w..@-4w' Date *intervals*, like the numeric intervals described above, are separated by two dots (``..``). You can specify a start, an end, or both. @@ -200,7 +206,6 @@ Find all items with a file modification time between 2008-12-01 and $ beet ls 'mtime:2008-12-01..2008-12-02' - .. _not_query: Query Term Negation diff --git a/setup.py b/setup.py index f6e686d35..1019a74a7 100755 --- a/setup.py +++ b/setup.py @@ -106,7 +106,7 @@ setup( 'pyxdg', 'pathlib', 'python-mpd2', - 'discogs-client', + 'discogs-client' ], # Plugin (optional) dependencies: From f0aca5e0d352b1273db464f910948f851311784b Mon Sep 17 00:00:00 2001 From: euri10 Date: Mon, 30 Jan 2017 09:06:26 +0100 Subject: [PATCH 14/15] Explain relative dates The previous version wasn't user-friendly enough and too technical. --- docs/reference/query.rst | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 6c0dae5ae..4e379f70f 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -163,16 +163,20 @@ and day are optional. If you leave out the day, for example, you will get matches for the whole month. You can also use relative dates to the current time. -A relative date begins by ``@`` and has to follow the pattern_dq format -``@([+|-]?)(\d+)([y|m|w|d])`` -- ``@`` indicates it's a date relative to now() -- the optional ``+`` or ``-`` sign, which defaults to ``+`` will increment or -decrement now() by a certain quantity -- that quantity can be expressed in days, weeks, months or years respectively -'d', 'w', 'm', 'y' +A relative date begins with an ``@``. +It looks like ``@-3w``, ``@2m`` or ``@-4d`` which means the date 3 weeks ago, +the date 2 months from now and the date 4 days ago. +A relative date consists of four parts: +- ``@`` indicates it's a date relative from now +- ``+`` or ``-`` sign is optional and defaults to ``+``. The ``+`` sign will +add a time quantity to the current date while the ``-`` sign will do the +opposite +- a number follows and indicates the amount to add or substract +- a final letter ends and represents the amount in either days, weeks, months or +years (``d``, ``w``, ``m`` or ``y``) -Please note that this relative calculation is rather approximate as it makes -the assumption of 30 days per month and 365 days per year. +Please note that this relative calculation makes the assumption of 30 days per +month and 365 days per year. Here is an example that finds all the albums added between now and last week:: From d2cd4c0f218a5ba433d9d11e8693472c69496fbe Mon Sep 17 00:00:00 2001 From: euri10 Date: Tue, 31 Jan 2017 16:56:03 +0100 Subject: [PATCH 15/15] Change relative date's format to further simplify it A relative date doesn't need to be prefixed by @ anymore. The relative date pattern now displays named groups. Digits have been change to [0-9] to avoid other digit characters. Removed the @ character in tests. Updated subsequent documentation. --- beets/dbcore/query.py | 29 ++++++++++++++++------------- docs/reference/query.rst | 9 ++++----- test/test_datequery.py | 16 ++++++++-------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 337e3074c..51f011a1d 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -554,22 +554,25 @@ class Period(object): relative. An absolute date has to be like one of the date_formats '%Y' or '%Y-%m' or '%Y-%m-%d' - A relative date begins by '@ 'and has to follow the pattern_dq format - '@([+|-]?)(\d+)([y|m|w|d])' - - '@' indicates it's a date relative to now() - - the optional '+' or '-' sign, which defaults to '+' will increment or - decrement now() by a certain quantity - - that quantity can be expressed in days, weeks, months or years - respectively 'd', 'w', 'm', 'y' - Please note that this relative calculation is rather approximate as it - makes the assumption of 30 days per month and 365 days per year + A relative date consists of three parts: + - a ``+`` or ``-`` sign is optional and defaults to ``+``. The ``+`` + sign will add a time quantity to the current date while the ``-`` sign + will do the opposite + - a number follows and indicates the amount to add or substract + - a final letter ends and represents the amount in either days, weeks, + months or years (``d``, ``w``, ``m`` or ``y``) + Please note that this relative calculation makes the assumption of 30 + days per month and 365 days per year. """ - pattern_dq = '@([+|-]?)(\d+)([y|m|w|d])' + + pattern_dq = '(?P[+|-]?)(?P[0-9]+)(?P[y|m|w|d])' # noqa: E501 match_dq = re.match(pattern_dq, string) + # test if the string matches the relative date pattern, add the parsed + # quantity to now in that case if match_dq is not None: - sign = match_dq.group(1) - quantity = match_dq.group(2) - timespan = match_dq.group(3) + sign = match_dq.group('sign') + quantity = match_dq.group('quantity') + timespan = match_dq.group('timespan') multiplier = -1 if sign == '-' else 1 days = cls.relative[timespan] date = datetime.now() + multiplier * timedelta( diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 4e379f70f..c9678589f 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -166,8 +166,7 @@ You can also use relative dates to the current time. A relative date begins with an ``@``. It looks like ``@-3w``, ``@2m`` or ``@-4d`` which means the date 3 weeks ago, the date 2 months from now and the date 4 days ago. -A relative date consists of four parts: -- ``@`` indicates it's a date relative from now +A relative date consists of three parts: - ``+`` or ``-`` sign is optional and defaults to ``+``. The ``+`` sign will add a time quantity to the current date while the ``-`` sign will do the opposite @@ -180,11 +179,11 @@ month and 365 days per year. Here is an example that finds all the albums added between now and last week:: - $ beet ls -a 'added:@-1w..' + $ beet ls -a 'added:-1w..' -Find all items added in a 2 weeks period 4 weeks ago:: +Find all items added in a 2 weeks period 4 weeks ago:: - $ beet ls -a 'added:@-6w..@-4w' + $ beet ls -a 'added:-6w..-4w' Date *intervals*, like the numeric intervals described above, are separated by two dots (``..``). You can specify a start, an end, or both. diff --git a/test/test_datequery.py b/test/test_datequery.py index d0c18e229..0864cac23 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -47,8 +47,8 @@ class DateIntervalTest(unittest.TestCase): self.assertContains('..2001', '2001-12-31T23:59:59') self.assertExcludes('..2001', '2002-01-01T00:00:00') - self.assertContains('@-1d..@1d', _datepattern(datetime.now())) - self.assertExcludes('@-2d..@-1d', _datepattern(datetime.now())) + self.assertContains('-1d..1d', _datepattern(datetime.now())) + self.assertExcludes('-2d..-1d', _datepattern(datetime.now())) def test_day_precision_intervals(self): self.assertContains('2000-06-20..2000-06-20', '2000-06-20T00:00:00') @@ -168,37 +168,37 @@ class DateQueryTestRelativeMore(_common.LibTestCase): def test_relative(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '@-4' + timespan + '..@+4' + timespan) + query = DateQuery('added', '-4' + timespan + '..+4' + timespan) matched = self.lib.items(query) self.assertEqual(len(matched), 1) def test_relative_fail(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '@-2' + timespan + '..@-1' + timespan) + query = DateQuery('added', '-2' + timespan + '..-1' + timespan) matched = self.lib.items(query) self.assertEqual(len(matched), 0) def test_start_relative(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '@-4' + timespan + '..') + query = DateQuery('added', '-4' + timespan + '..') matched = self.lib.items(query) self.assertEqual(len(matched), 1) def test_start_relative_fail(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '@4' + timespan + '..') + query = DateQuery('added', '4' + timespan + '..') matched = self.lib.items(query) self.assertEqual(len(matched), 0) def test_end_relative(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '..@+4' + timespan) + query = DateQuery('added', '..+4' + timespan) matched = self.lib.items(query) self.assertEqual(len(matched), 1) def test_end_relative_fail(self): for timespan in ['d', 'w', 'm', 'y']: - query = DateQuery('added', '..@-4' + timespan) + query = DateQuery('added', '..-4' + timespan) matched = self.lib.items(query) self.assertEqual(len(matched), 0)