Repository: bimpression/sosw

Issue:
Scheduler: chunk_days() - more patterns
Support more generic patterns for chunking, e.g.:
- previous_X_days
- last_week
- previous_week
- yesterday
- today
- smart_X_of_Y_days_back (more often recent days, less and less often days that were long ago up to `Y`; the total number should be `X`)

Discussion:

Patch:
diff --git a/sosw/scheduler.py b/sosw/scheduler.py
index 5ba5ae0..33234a3 100644
--- a/sosw/scheduler.py
+++ b/sosw/scheduler.py
@@ -200,6 +200,23 @@ class Scheduler(Processor):
         return [str(today - datetime.timedelta(days=x)) for x in range(num, 0, -1)]
 
 
+    def previous_x_days(self, pattern: str) -> List[str]:
+        """
+        Returns a list of string dates from today - x - x
+
+        For example, consider today's date as 2019-04-30.
+        If I call for previous_x_days(pattern='previous_2_days'), I will receive a list of string dates equal to:
+        ['2019-04-26', '2019-04-27']
+        """
+        assert re.match('previous_[0-9]+_days', pattern) is not None, "Invalid pattern {pattern} for `previous_x_days()`"
+
+        num = int(pattern.split('_')[1])
+        today = datetime.date.today()
+        end_date = today - datetime.timedelta(days=num)
+
+        return [str(end_date - datetime.timedelta(days=x)) for x in range(num, 0, -1)]
+
+
     def x_days_back(self, pattern: str) -> List[str]:
         """
         Finds the exact date X days back from now.
@@ -216,6 +233,38 @@ class Scheduler(Processor):
         return [str(today - datetime.timedelta(days=num))]
 
 
+    def yesterday(self, pattern: str = 'yesterday') -> List[str]:
+        """
+        Simple wrapper for x_days_back() to return yesterday's date.
+        """
+        assert re.match('yesterday', pattern) is not None, "Invalid pattern {pattern} for `yesterday()`"
+        return self.x_days_back('1_days_back')
+
+
+    def today(self, pattern: str = 'today') -> List[str]:
+        """
+        Returns list with one datetime string (YYYY-MM-DD) equal to today's date.
+        """
+        assert re.match('today', pattern) is not None, "Invalid pattern {pattern} for `today()`"
+        return [str(datetime.date.today())]
+
+
+    def last_week(self, pattern: str = 'last_week') -> List[str]:
+        """
+        Returns list of dates (YYYY-MM-DD) as strings for last week (Sunday - Saturday)
+        :param pattern:
+        :return:
+        """
+        assert re.match('last_week', pattern) is not None, "Invalid pattern {pattern} for `last_week()`"
+
+        today = datetime.date.today()
+        end_date = today - datetime.timedelta(days=today.weekday() + 8)
+
+        return [str(end_date + datetime.timedelta(days=x)) for x in range(7)]
+
+
+
+
     def chunk_dates(self, job: Dict, skeleton: Dict = None) -> List[Dict]:
         """
         There is a support for multiple not nested parameters to chunk. Dates is one very specific of them.
@@ -228,7 +277,7 @@ class Scheduler(Processor):
         period = job.pop('period', None)
         isolate = job.pop('isolate_days', None)
 
-        PERIOD_KEYS = ['last_[0-9]+_days', '[0-9]+_days_back']  # , 'yesterday']
+        PERIOD_KEYS = ['last_[0-9]+_days', '[0-9]+_days_back', 'yesterday', 'today', 'previous_[0-9]+_days', 'last_week']
 
         if period:
 
@@ -242,7 +291,7 @@ class Scheduler(Processor):
                     break
             else:
                 raise ValueError(f"Unsupported period requested: {period}. Valid options are: "
-                                 f"'last_X_days', 'X_days_back'")
+                                 f"'last_X_days', 'X_days_back', 'yesterday', 'today', 'previous_[0-9]+_days', 'last_week'")
 
             if isolate:
                 assert len(date_list) > 0, f"The chunking period: {period} did not generate date_list. Bad."


Test patch:
diff --git a/sosw/test/unit/test_scheduler.py b/sosw/test/unit/test_scheduler.py
index baf0701..25fec9e 100644
--- a/sosw/test/unit/test_scheduler.py
+++ b/sosw/test/unit/test_scheduler.py
@@ -275,6 +275,32 @@ class Scheduler_UnitTestCase(unittest.TestCase):
 
 
     ### Tests of chunk_dates ###
+    def test_chunk_dates(self):
+        TESTS = [
+            ({'period': 'today'}, 'today'),
+            ({'period': 'yesterday'}, 'yesterday'),
+            ({'period': 'last_3_days'}, 'last_x_days'),
+            ({'period': '10_days_back'}, 'x_days_back'),
+            ({'period': 'previous_2_days'}, 'previous_x_days'),
+            ({'period': 'last_week'}, 'last_week')
+        ]
+
+        for test, func_name in TESTS:
+            FUNCTIONS = ['today', 'yesterday', 'last_x_days', 'x_days_back', 'previous_x_days', 'last_week']
+            for f in FUNCTIONS:
+                setattr(self.scheduler, f, MagicMock())
+
+            self.scheduler.chunk_dates(test)
+
+            func = getattr(self.scheduler, func_name)
+            func.assert_called_once()
+
+            for bad_f_name in [x for x in FUNCTIONS if not x == func_name]:
+                bad_f = getattr(self.scheduler, bad_f_name)
+                bad_f.assert_not_called()
+
+
+
     def test_chunk_dates__preserve_skeleton(self):
         TESTS = [
             {'period': 'last_1_days', 'a': 'foo'},
@@ -384,6 +410,66 @@ class Scheduler_UnitTestCase(unittest.TestCase):
         self.assertEqual(today.weekday(), datetime.datetime.strptime(last_week, '%Y-%m-%d').weekday())
 
 
+    def test_yesterday(self):
+
+        TESTS = [
+            ('yesterday', ['2019-04-10']),
+        ]
+
+        today = datetime.date(2019, 4, 11)
+
+        with patch('sosw.scheduler.datetime.date') as mdt:
+            mdt.today.return_value = today
+
+            for test, expected in TESTS:
+                self.assertEqual(self.scheduler.yesterday(test), expected)
+
+    def test_today(self):
+        TESTS = [
+            ('today', ['2019-04-10']),
+        ]
+        today = datetime.date(2019, 4, 10)
+
+        with patch('sosw.scheduler.datetime.date') as mdt:
+            mdt.today.return_value = today
+
+            for test, expected in TESTS:
+                self.assertEqual(self.scheduler.today(test), expected)
+
+    def test_previous_x_days(self):
+        today = datetime.date(2019, 4, 30)
+
+        TESTS = [
+            ('previous_2_days', ['2019-04-26', '2019-04-27']),
+            ('previous_3_days', ['2019-04-24', '2019-04-25', '2019-04-26'])
+        ]
+
+        with patch('sosw.scheduler.datetime.date') as mdt:
+            mdt.today.return_value = today
+
+            for test, expected in TESTS:
+                self.assertEqual(self.scheduler.previous_x_days(test), expected)
+
+    def test_last_week(self):
+        today = datetime.date(2019, 4, 30)
+
+        TESTS = [
+            ('last_week', ['2019-04-21',
+                           '2019-04-22',
+                           '2019-04-23',
+                           '2019-04-24',
+                           '2019-04-25',
+                           '2019-04-26',
+                           '2019-04-27'])
+        ]
+
+        with patch('sosw.scheduler.datetime.date') as mdt:
+            mdt.today.return_value = today
+
+            for test, expected in TESTS:
+                self.assertEqual(self.scheduler.last_week(test), expected)
+
+
     ### Tests of chunk_job ###
     def test_chunk_job__not_chunkable_config(self):
         self.scheduler.chunkable_attrs = []