From 47b0b54fa1a6d76c5035f82b4cde9903f4381d7c Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 28 Jan 2017 19:15:21 +0100 Subject: [PATCH 01/24] 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 470ca2ac6e..8d778074f2 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -21,6 +21,7 @@ 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 @@ def parse(cls, string): """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 @@ def open_right_endpoint(self): """ 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 43bff8014a..477e3c874c 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/24] trying relative dates --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1019a74a76..ec582105fc 100755 --- a/setup.py +++ b/setup.py @@ -106,7 +106,8 @@ def build_manpages(): '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/24] 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 ec582105fc..cd39111341 100755 --- a/setup.py +++ b/setup.py @@ -107,7 +107,7 @@ def build_manpages(): 'pathlib', 'python-mpd2', 'discogs-client', - 'dateutils' + 'python-dateutil' ], # Plugin (optional) dependencies: diff --git a/tox.ini b/tox.ini index 477e3c874c..8ba69faff6 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/24] 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 8d778074f2..dcbecd5950 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -561,13 +561,13 @@ def parse(cls, string): 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/24] need dateutil in install ? --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cd39111341..672402603c 100755 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ def build_manpages(): '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 @@ def build_manpages(): '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/24] 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 1e1625db27..4b01edd2cd 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 @@ def test_year_precision_intervals(self): 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 @@ def test_unbounded_endpoints(self): 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 @@ def test_single_day_nonmatch_fast(self): 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/24] 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 2f3366d4c1..c2a5cdf0a6 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 4b01edd2cd..e2d1d23a39 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -123,6 +123,7 @@ def test_single_day_nonmatch_fast(self): 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 @@ def test_single_month_match_fast(self): 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 @@ def test_single_month_match_slow(self): 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 @@ def test_single_day_match_fast(self): 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/24] 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 e2d1d23a39..95e6170840 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -70,8 +70,7 @@ def test_unbounded_endpoints(self): 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/24] 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 dcbecd5950..0b0cfa6f7d 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -21,7 +21,6 @@ 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 @@ def parse(cls, string): 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 672402603c..f6e686d35f 100755 --- a/setup.py +++ b/setup.py @@ -93,7 +93,6 @@ def build_manpages(): '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 @@ def build_manpages(): 'pathlib', 'python-mpd2', 'discogs-client', - 'python-dateutil', ], # Plugin (optional) dependencies: diff --git a/test/test_datequery.py b/test/test_datequery.py index 95e6170840..cefc03b614 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 @@ def test_single_month_match_fast(self): 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 @@ def test_single_month_match_slow(self): 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 @@ def test_single_day_match_fast(self): 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 8ba69faff6..43bff8014a 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/24] 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 0b0cfa6f7d..76679b021f 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -551,10 +551,11 @@ def parse(cls, string): """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 @@ def parse(cls, string): 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 cefc03b614..e74c9a0db1 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -160,6 +160,49 @@ def test_single_day_nonmatch_fast(self): 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/24] 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 e74c9a0db1..3e546b1959 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -168,13 +168,13 @@ def setUp(self): 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/24] 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 76679b021f..337e3074c1 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 @@ def __init__(self, date, precision): 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 c2a5cdf0a6..da97b0d873 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 3e546b1959..d0c18e2294 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -168,7 +168,7 @@ def setUp(self): 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/24] 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 da97b0d873..6c0dae5aef 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 f6e686d35f..1019a74a76 100755 --- a/setup.py +++ b/setup.py @@ -106,7 +106,7 @@ def build_manpages(): '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/24] Explain relative dates The previous version wasn't user-friendly enough and too technical. --- docs/reference/query.rst | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 6c0dae5aef..4e379f70f3 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' - -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 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 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/24] 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 337e3074c1..51f011a1df 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -554,22 +554,25 @@ def parse(cls, string): 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 4e379f70f3..c9678589fa 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 d0c18e2294..0864cac239 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -47,8 +47,8 @@ def test_year_precision_intervals(self): 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 @@ def setUp(self): 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) From 92c118f4a85823c3a90ef414b5c05f63656e355f Mon Sep 17 00:00:00 2001 From: euri10 Date: Thu, 15 Jun 2017 09:11:28 +0200 Subject: [PATCH 16/24] Corrected some flake8 errors --- beets/dbcore/query.py | 49 ++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index e90077c5fd..d669b34c4f 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -40,6 +40,7 @@ class InvalidQueryError(ParsingError): The query should be a unicode string or a list, which will be space-joined. """ + def __init__(self, query, explanation): if isinstance(query, list): query = " ".join(query) @@ -53,6 +54,7 @@ class InvalidQueryArgumentValueError(ParsingError): It exists to be caught in upper stack levels so a meaningful (i.e. with the query) InvalidQueryError can be raised. """ + def __init__(self, what, expected, detail=None): message = u"'{0}' is not {1}".format(what, expected) if detail: @@ -63,6 +65,7 @@ def __init__(self, what, expected, detail=None): class Query(object): """An abstract class representing a query into the item database. """ + def clause(self): """Generate an SQLite expression implementing the query. @@ -95,6 +98,7 @@ class FieldQuery(Query): string. Subclasses may also provide `col_clause` to implement the same matching functionality in SQLite. """ + def __init__(self, field, pattern, fast=True): self.field = field self.pattern = pattern @@ -126,7 +130,7 @@ def __repr__(self): def __eq__(self, other): return super(FieldQuery, self).__eq__(other) and \ - self.field == other.field and self.pattern == other.pattern + self.field == other.field and self.pattern == other.pattern def __hash__(self): return hash((self.field, hash(self.pattern))) @@ -134,6 +138,7 @@ def __hash__(self): class MatchQuery(FieldQuery): """A query that looks for exact matches in an item field.""" + def col_clause(self): return self.field + " = ?", [self.pattern] @@ -143,7 +148,6 @@ def value_match(cls, pattern, value): class NoneQuery(FieldQuery): - def __init__(self, field, fast=True): super(NoneQuery, self).__init__(field, None, fast) @@ -165,6 +169,7 @@ class StringFieldQuery(FieldQuery): """A FieldQuery that converts values to strings before matching them. """ + @classmethod def value_match(cls, pattern, value): """Determine whether the value matches the pattern. The value @@ -182,11 +187,12 @@ def string_match(cls, pattern, value): class SubstringQuery(StringFieldQuery): """A query that matches a substring in a specific item field.""" + def col_clause(self): pattern = (self.pattern - .replace('\\', '\\\\') - .replace('%', '\\%') - .replace('_', '\\_')) + .replace('\\', '\\\\') + .replace('%', '\\%') + .replace('_', '\\_')) search = '%' + pattern + '%' clause = self.field + " like ? escape '\\'" subvals = [search] @@ -204,6 +210,7 @@ class RegexpQuery(StringFieldQuery): Raises InvalidQueryError when the pattern is not a valid regular expression. """ + def __init__(self, field, pattern, fast=True): super(RegexpQuery, self).__init__(field, pattern, fast) pattern = self._normalize(pattern) @@ -231,6 +238,7 @@ class BooleanQuery(MatchQuery): """Matches a boolean field. Pattern should either be a boolean or a string reflecting a boolean. """ + def __init__(self, field, pattern, fast=True): super(BooleanQuery, self).__init__(field, pattern, fast) if isinstance(pattern, six.string_types): @@ -244,6 +252,7 @@ class BytesQuery(MatchQuery): `unicode` equivalently in Python 2. Always use this query instead of `MatchQuery` when matching on BLOB values. """ + def __init__(self, field, pattern): super(BytesQuery, self).__init__(field, pattern) @@ -270,6 +279,7 @@ class NumericQuery(FieldQuery): Raises InvalidQueryError when the pattern does not represent an int or a float. """ + def _convert(self, s): """Convert a string to a numeric type (float or int). @@ -337,6 +347,7 @@ class CollectionQuery(Query): """An abstract query class that aggregates other queries. Can be indexed like a list to access the sub-queries. """ + def __init__(self, subqueries=()): self.subqueries = subqueries @@ -375,7 +386,7 @@ def __repr__(self): def __eq__(self, other): return super(CollectionQuery, self).__eq__(other) and \ - self.subqueries == other.subqueries + self.subqueries == other.subqueries def __hash__(self): """Since subqueries are mutable, this object should not be hashable. @@ -389,6 +400,7 @@ class AnyFieldQuery(CollectionQuery): any field. The individual field query class is provided to the constructor. """ + def __init__(self, pattern, fields, cls): self.pattern = pattern self.fields = fields @@ -414,7 +426,7 @@ def __repr__(self): def __eq__(self, other): return super(AnyFieldQuery, self).__eq__(other) and \ - self.query_class == other.query_class + self.query_class == other.query_class def __hash__(self): return hash((self.pattern, tuple(self.fields), self.query_class)) @@ -424,6 +436,7 @@ class MutableCollectionQuery(CollectionQuery): """A collection query whose subqueries may be modified after the query is initialized. """ + def __setitem__(self, key, value): self.subqueries[key] = value @@ -433,6 +446,7 @@ def __delitem__(self, key): class AndQuery(MutableCollectionQuery): """A conjunction of a list of other queries.""" + def clause(self): return self.clause_with_joiner('and') @@ -442,6 +456,7 @@ def match(self, item): class OrQuery(MutableCollectionQuery): """A conjunction of a list of other queries.""" + def clause(self): return self.clause_with_joiner('or') @@ -453,6 +468,7 @@ class NotQuery(Query): """A query that matches the negation of its `subquery`, as a shorcut for performing `not(subquery)` without using regular expressions. """ + def __init__(self, subquery): self.subquery = subquery @@ -473,7 +489,7 @@ def __repr__(self): def __eq__(self, other): return super(NotQuery, self).__eq__(other) and \ - self.subquery == other.subquery + self.subquery == other.subquery def __hash__(self): return hash(('not', hash(self.subquery))) @@ -481,6 +497,7 @@ def __hash__(self): class TrueQuery(Query): """A query that always matches.""" + def clause(self): return '1', () @@ -490,6 +507,7 @@ def match(self, item): class FalseQuery(Query): """A query that never matches.""" + def clause(self): return '0', () @@ -533,7 +551,6 @@ class Period(object): instants of time during January 2014. """ - precisions = ('year', 'month', 'day', 'hour', 'minute', 'second') date_formats = ( ('%Y',), # year @@ -545,7 +562,6 @@ class Period(object): ) 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 precision (a string, one of "year", "month", "day", "hour", "minute", @@ -599,7 +615,8 @@ def find_date_and_format(string): timespan = match_dq.group('timespan') multiplier = -1 if sign == '-' else 1 days = cls.relative[timespan] - date = datetime.now() + multiplier * timedelta(days=int(quantity) * days) + date = datetime.now() + multiplier * timedelta( + days=int(quantity) * days) string = date.strftime(cls.date_formats[5][0]) date, ordinal = find_date_and_format(string) @@ -838,13 +855,14 @@ def __hash__(self): def __eq__(self, other): return super(MultipleSort, self).__eq__(other) and \ - self.sorts == other.sorts + self.sorts == other.sorts class FieldSort(Sort): """An abstract sort criterion that orders by a specific field (of any kind). """ + def __init__(self, field, ascending=True, case_insensitive=True): self.field = field self.ascending = ascending @@ -875,13 +893,14 @@ def __hash__(self): def __eq__(self, other): return super(FieldSort, self).__eq__(other) and \ - self.field == other.field and \ - self.ascending == other.ascending + self.field == other.field and \ + self.ascending == other.ascending class FixedFieldSort(FieldSort): """Sort object to sort on a fixed field. """ + def order_clause(self): order = "ASC" if self.ascending else "DESC" if self.case_insensitive: @@ -898,12 +917,14 @@ class SlowFieldSort(FieldSort): """A sort criterion by some model field other than a fixed field: i.e., a computed or flexible field. """ + def is_slow(self): return True class NullSort(Sort): """No sorting. Leave results unsorted.""" + def sort(self, items): return items From 527d0a88f4221663a31b433ef98c52cb4c46b022 Mon Sep 17 00:00:00 2001 From: euri10 Date: Thu, 15 Jun 2017 09:31:41 +0200 Subject: [PATCH 17/24] Corrected some flake8 errors --- beets/dbcore/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index d669b34c4f..169697b12b 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -130,7 +130,7 @@ def __repr__(self): def __eq__(self, other): return super(FieldQuery, self).__eq__(other) and \ - self.field == other.field and self.pattern == other.pattern + self.field == other.field and self.pattern == other.pattern def __hash__(self): return hash((self.field, hash(self.pattern))) From ae3f9bf9b6c162a7f9386eab45219d4c442f7e48 Mon Sep 17 00:00:00 2001 From: euri10 Date: Thu, 15 Jun 2017 09:59:53 +0200 Subject: [PATCH 18/24] Corrected some flake8 errors, overindentation and pycharm reformat don't seem to play well --- beets/dbcore/query.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 169697b12b..14117f9af1 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -386,7 +386,7 @@ def __repr__(self): def __eq__(self, other): return super(CollectionQuery, self).__eq__(other) and \ - self.subqueries == other.subqueries + self.subqueries == other.subqueries def __hash__(self): """Since subqueries are mutable, this object should not be hashable. @@ -426,7 +426,7 @@ def __repr__(self): def __eq__(self, other): return super(AnyFieldQuery, self).__eq__(other) and \ - self.query_class == other.query_class + self.query_class == other.query_class def __hash__(self): return hash((self.pattern, tuple(self.fields), self.query_class)) @@ -489,7 +489,7 @@ def __repr__(self): def __eq__(self, other): return super(NotQuery, self).__eq__(other) and \ - self.subquery == other.subquery + self.subquery == other.subquery def __hash__(self): return hash(('not', hash(self.subquery))) @@ -855,7 +855,7 @@ def __hash__(self): def __eq__(self, other): return super(MultipleSort, self).__eq__(other) and \ - self.sorts == other.sorts + self.sorts == other.sorts class FieldSort(Sort): @@ -893,8 +893,8 @@ def __hash__(self): def __eq__(self, other): return super(FieldSort, self).__eq__(other) and \ - self.field == other.field and \ - self.ascending == other.ascending + self.field == other.field and \ + self.ascending == other.ascending class FixedFieldSort(FieldSort): From 8a6c8cde6a6221274c74722a47b26d4ae70f6cb0 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 15 Jun 2017 18:10:55 -0400 Subject: [PATCH 19/24] Simplify the docs a bit for relative dates --- beets/dbcore/query.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 39ff0dc771..2feec08abe 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -577,20 +577,19 @@ def __init__(self, date, precision): @classmethod 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 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. + string is empty, or raise an InvalidQueryArgumentValueError if + the string cannot be parsed to a date. + + The date may be absolute or relative. Absolute dates look like + `YYYY`, or `YYYY-MM-DD`, or `YYYY-MM-DD HH:MM:SS`, etc. Relative + dates have three parts: + + - Optionally, a ``+`` or ``-`` sign indicating the future or the + past. The default is the future. + - A number: how much to add or subtract. + - A letter indicating the unit: days, weeks, months or years + (``d``, ``w``, ``m`` or ``y``). A "month" is exactly 30 days + and a "year" is exactly 365 days. """ def find_date_and_format(string): From 130c581c282942e01c0ae4c50cc82d2ff070414e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 15 Jun 2017 18:15:51 -0400 Subject: [PATCH 20/24] Some formatting cleanup for relative dates --- beets/dbcore/query.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 2feec08abe..5ef440b5e4 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -563,6 +563,8 @@ class Period(object): ('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S') # second ) relative = {'y': 365, 'm': 30, 'w': 7, 'd': 1} + relative_re = '(?P[+|-]?)(?P[0-9]+)' + \ + '(?P[y|m|w|d])' def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and @@ -606,24 +608,26 @@ def find_date_and_format(string): if not string: return None - 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: + # Check for a relative date. + match_dq = re.match(cls.relative_re, string) + if match_dq: sign = match_dq.group('sign') quantity = match_dq.group('quantity') timespan = match_dq.group('timespan') + + # Add or subtract the given amount of time from the current + # date. multiplier = -1 if sign == '-' else 1 days = cls.relative[timespan] - date = datetime.now() + multiplier * timedelta( - days=int(quantity) * days) + date = datetime.now() + \ + timedelta(days=int(quantity) * days) * multiplier string = date.strftime(cls.date_formats[5][0]) + # Check for an absolute date. date, ordinal = find_date_and_format(string) if date is None: raise InvalidQueryArgumentValueError(string, - 'a valid datetime string') + 'a valid date/time string') precision = cls.precisions[ordinal] return cls(date, precision) From 93f064f505837333f2090338df20f6430bc5ab01 Mon Sep 17 00:00:00 2001 From: euri10 Date: Fri, 16 Jun 2017 09:14:40 +0200 Subject: [PATCH 21/24] Renamed relative to relative_units Changed the return in case of a relative date, by default precision is to the second, aka 5 Removed old 'relative' precision that indeed isn't necessary anymore Corrected doc according to current relative date format, no more @ --- beets/dbcore/query.py | 8 +++----- docs/reference/query.rst | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 5ef440b5e4..fbe6626c7c 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -562,7 +562,7 @@ class Period(object): ('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M'), # minute ('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S') # second ) - relative = {'y': 365, 'm': 30, 'w': 7, 'd': 1} + relative_units = {'y': 365, 'm': 30, 'w': 7, 'd': 1} relative_re = '(?P[+|-]?)(?P[0-9]+)' + \ '(?P[y|m|w|d])' @@ -618,10 +618,10 @@ def find_date_and_format(string): # Add or subtract the given amount of time from the current # date. multiplier = -1 if sign == '-' else 1 - days = cls.relative[timespan] + days = cls.relative_units[timespan] date = datetime.now() + \ timedelta(days=int(quantity) * days) * multiplier - string = date.strftime(cls.date_formats[5][0]) + return cls(date, cls.precisions[5]) # Check for an absolute date. date, ordinal = find_date_and_format(string) @@ -637,8 +637,6 @@ def open_right_endpoint(self): """ 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/docs/reference/query.rst b/docs/reference/query.rst index fa1286c8c7..bb14f3cd27 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -165,8 +165,7 @@ 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 with an ``@``. -It looks like ``@-3w``, ``@2m`` or ``@-4d`` which means the date 3 weeks ago, +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 three parts: - ``+`` or ``-`` sign is optional and defaults to ``+``. The ``+`` sign will From 690ed731d83e393a93d5dea1e892d79a7c557578 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 16 Jun 2017 11:07:24 -0400 Subject: [PATCH 22/24] Move the relative date docs downward It's important to introduce the .. syntax before we get here. --- docs/reference/query.rst | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index bb14f3cd27..4eba910ca5 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -164,28 +164,6 @@ 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. -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 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 -- 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. - -Here is an example that finds all the albums added between now and last week:: - - $ beet ls -a 'added:-1w..' - -Find all items added in a 2 weeks period 4 weeks ago:: - - $ 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. @@ -239,6 +217,28 @@ queries do the same thing:: $ beet ls 'added:2008-12-01t22:45:20' $ beet ls 'added:2008-12-01 22:45:20' +You can also use relative dates to the current time. +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 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 +- 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. + +Here is an example that finds all the albums added between now and last week:: + + $ beet ls -a 'added:-1w..' + +Find all items added in a 2 weeks period 4 weeks ago:: + + $ beet ls -a 'added:-6w..-4w' + .. _not_query: Query Term Negation From b1b4272501a9e5c0fb4f89fed831b69fa1f77d29 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 16 Jun 2017 11:11:12 -0400 Subject: [PATCH 23/24] Refinements to the relative date docs --- docs/reference/query.rst | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 4eba910ca5..d103d9aec8 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -217,27 +217,24 @@ queries do the same thing:: $ beet ls 'added:2008-12-01t22:45:20' $ beet ls 'added:2008-12-01 22:45:20' -You can also use relative dates to the current time. -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 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 -- 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. - -Here is an example that finds all the albums added between now and last week:: +You can also use *relative* dates. For example, ``-3w`` means three weeks ago, +and ``+4d`` means four days in the future. A relative date has three parts: + +- Either ``+`` or ``-``, to indicate the past or the future. The sign is + optional; if you leave this off, it defaults to the future. +- A number. +- A letter indicating the unit: ``d``, ``w``, ``m`` or ``y``, meaning days, + weeks, months or years. (A "month" is always 30 days and a "year" is always + 365 days.) + +Here's an example that finds all the albums added since last week:: $ beet ls -a 'added:-1w..' -Find all items added in a 2 weeks period 4 weeks ago:: +And here's an example that lists items added in a two-week period starting +four weeks ago:: - $ beet ls -a 'added:-6w..-4w' + $ beet ls 'added:-6w..-4w' .. _not_query: From a52d3d532ad01f7f7d5798baa0688b7b14576d6c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 16 Jun 2017 11:13:01 -0400 Subject: [PATCH 24/24] Changelog for relative dates (#2418, #2598) --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 01cc27cc56..d692c4bf86 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,9 @@ Features: * :ref:`Date queries ` can now include times, so you can filter your music down to the second. Thanks to :user:`discopatrick`. :bug:`2506` :bug:`2528` +* :ref:`Date queries ` can also be *relative*. You can say + ``added:-1w..`` to match music added in the last week, for example. Thanks + to :user:`euri10`. :bug:`2598` * A new :doc:`/plugins/gmusic` lets you interact with your Google Play Music library. Thanks to :user:`tigranl`. :bug:`2553` :bug:`2586` * :doc:`/plugins/replaygain`: We now keep R128 data in separate tags from