Repository: spotify/luigi

Issue:
Adding DateSecondParameter to parameter.py
Hi,

Would you be open to such addition ?

I can send a PR replicating the existing logic for hours & minutes:
https://github.com/spotify/luigi/blob/master/luigi/parameter.py#L477



Discussion:
sure, would make sense!


Resolving patch:
diff --git a/luigi/__init__.py b/luigi/__init__.py
--- a/luigi/__init__.py
+++ b/luigi/__init__.py
@@ -32,7 +32,7 @@
 from luigi import parameter
 from luigi.parameter import (
     Parameter,
-    DateParameter, MonthParameter, YearParameter, DateHourParameter, DateMinuteParameter,
+    DateParameter, MonthParameter, YearParameter, DateHourParameter, DateMinuteParameter, DateSecondParameter,
     DateIntervalParameter, TimeDeltaParameter,
     IntParameter, FloatParameter, BooleanParameter, BoolParameter,
     TaskParameter, EnumParameter, DictParameter, ListParameter, TupleParameter
@@ -53,7 +53,7 @@
     'task', 'Task', 'Config', 'ExternalTask', 'WrapperTask', 'namespace',
     'target', 'Target', 'File', 'LocalTarget', 'rpc', 'RemoteScheduler',
     'RPCError', 'parameter', 'Parameter', 'DateParameter', 'MonthParameter',
-    'YearParameter', 'DateHourParameter', 'DateMinuteParameter', 'range',
+    'YearParameter', 'DateHourParameter', 'DateMinuteParameter', 'DateSecondParameter', 'range',
     'DateIntervalParameter', 'TimeDeltaParameter', 'IntParameter',
     'FloatParameter', 'BooleanParameter', 'BoolParameter', 'TaskParameter',
     'ListParameter', 'TupleParameter', 'EnumParameter', 'DictParameter',
diff --git a/luigi/parameter.py b/luigi/parameter.py
--- a/luigi/parameter.py
+++ b/luigi/parameter.py
@@ -502,6 +502,21 @@ def parse(self, s):
             return super(DateMinuteParameter, self).parse(s)
 
 
+class DateSecondParameter(_DatetimeParameterBase):
+    """
+    Parameter whose value is a :py:class:`~datetime.datetime` specified to the second.
+
+    A DateSecondParameter is a `ISO 8601 <http://en.wikipedia.org/wiki/ISO_8601>`_ formatted
+    date and time specified to the second. For example, ``2013-07-10T190738`` specifies July 10, 2013 at
+    19:07:38.
+
+    The interval parameter can be used to clamp this parameter to every N seconds, instead of every second.
+    """
+
+    date_format = '%Y-%m-%dT%H%M%S'
+    _timedelta = datetime.timedelta(seconds=1)
+
+
 class IntParameter(Parameter):
     """
     Parameter whose value is an ``int``.
diff --git a/luigi/util.py b/luigi/util.py
--- a/luigi/util.py
+++ b/luigi/util.py
@@ -415,6 +415,8 @@ def previous(task):
 
         if isinstance(param_obj, parameter.DateParameter):
             previous_date_params[param_name] = param_value - datetime.timedelta(days=1)
+        elif isinstance(param_obj, parameter.DateSecondParameter):
+            previous_date_params[param_name] = param_value - datetime.timedelta(seconds=1)
         elif isinstance(param_obj, parameter.DateMinuteParameter):
             previous_date_params[param_name] = param_value - datetime.timedelta(minutes=1)
         elif isinstance(param_obj, parameter.DateHourParameter):


Test patch:
diff --git a/test/date_parameter_test.py b/test/date_parameter_test.py
--- a/test/date_parameter_test.py
+++ b/test/date_parameter_test.py
@@ -34,6 +34,10 @@ class DateMinuteTask(luigi.Task):
     dm = luigi.DateMinuteParameter()
 
 
+class DateSecondTask(luigi.Task):
+    ds = luigi.DateSecondParameter()
+
+
 class MonthTask(luigi.Task):
     month = luigi.MonthParameter()
 
@@ -108,6 +112,24 @@ def test_serialize_task(self):
         self.assertEqual(str(t), 'DateMinuteTask(dm=2013-02-01T1842)')
 
 
+class DateSecondParameterTest(unittest.TestCase):
+    def test_parse(self):
+        ds = luigi.DateSecondParameter().parse('2013-02-01T184227')
+        self.assertEqual(ds, datetime.datetime(2013, 2, 1, 18, 42, 27))
+
+    def test_serialize(self):
+        ds = luigi.DateSecondParameter().serialize(datetime.datetime(2013, 2, 1, 18, 42, 27))
+        self.assertEqual(ds, '2013-02-01T184227')
+
+    def test_parse_interface(self):
+        in_parse(["DateSecondTask", "--ds", "2013-02-01T184227"],
+                 lambda task: self.assertEqual(task.ds, datetime.datetime(2013, 2, 1, 18, 42, 27)))
+
+    def test_serialize_task(self):
+        t = DateSecondTask(datetime.datetime(2013, 2, 1, 18, 42, 27))
+        self.assertEqual(str(t), 'DateSecondTask(ds=2013-02-01T184227)')
+
+
 class MonthParameterTest(unittest.TestCase):
     def test_parse(self):
         m = luigi.MonthParameter().parse('2015-04')
diff --git a/test/import_test.py b/test/import_test.py
--- a/test/import_test.py
+++ b/test/import_test.py
@@ -60,7 +60,7 @@ def import_luigi_test(self):
             luigi.RPCError,
             luigi.run, luigi.build,
             luigi.Parameter,
-            luigi.DateHourParameter, luigi.DateMinuteParameter, luigi.DateParameter,
+            luigi.DateHourParameter, luigi.DateMinuteParameter, luigi.DateSecondParameter, luigi.DateParameter,
             luigi.MonthParameter, luigi.YearParameter,
             luigi.DateIntervalParameter, luigi.TimeDeltaParameter,
             luigi.IntParameter, luigi.FloatParameter,
diff --git a/test/parameter_test.py b/test/parameter_test.py
--- a/test/parameter_test.py
+++ b/test/parameter_test.py
@@ -543,6 +543,16 @@ def testDateMinuteDeprecated(self):
         p = luigi.DateMinuteParameter(config_path=dict(section="foo", name="bar"))
         self.assertEqual(datetime.datetime(2001, 2, 3, 4, 30, 0), _value(p))
 
+    @with_config({"foo": {"bar": "2001-02-03T040506"}})
+    def testDateSecond(self):
+        p = luigi.DateSecondParameter(config_path=dict(section="foo", name="bar"))
+        self.assertEqual(datetime.datetime(2001, 2, 3, 4, 5, 6), _value(p))
+
+    @with_config({"foo": {"bar": "2001-02-03T040507"}})
+    def testDateSecondWithInterval(self):
+        p = luigi.DateSecondParameter(config_path=dict(section="foo", name="bar"), interval=2)
+        self.assertEqual(datetime.datetime(2001, 2, 3, 4, 5, 6), _value(p))
+
     @with_config({"foo": {"bar": "2001-02-03"}})
     def testDate(self):
         p = luigi.DateParameter(config_path=dict(section="foo", name="bar"))
diff --git a/test/util_previous_test.py b/test/util_previous_test.py
--- a/test/util_previous_test.py
+++ b/test/util_previous_test.py
@@ -80,7 +80,7 @@ class DateMinuteTaskOk(luigi.Task):
 
     def complete(self):
         # test against 2000.03.01T02H03
-        return self.minute in [datetime.datetime(2000, 3, 1, 2, 0), datetime.datetime(2000, 3, 1, 2, 3), datetime.datetime(2000, 3, 1, 2, 4)]
+        return self.minute in [datetime.datetime(2000, 3, 1, 2, 0)]
 
 
 class DateMinuteTaskOkTest(unittest.TestCase):
@@ -101,6 +101,31 @@ def test_get_previous_completed_not_found(self):
         self.assertEqual(None, prev)
 
 
+class DateSecondTaskOk(luigi.Task):
+    second = luigi.DateSecondParameter()
+
+    def complete(self):
+        return self.second in [datetime.datetime(2000, 3, 1, 2, 3, 4)]
+
+
+class DateSecondTaskOkTest(unittest.TestCase):
+
+    def test_previous(self):
+        task = DateSecondTaskOk(datetime.datetime(2000, 3, 1, 2, 3, 7))
+        prev = previous(task)
+        self.assertEqual(prev.second, datetime.datetime(2000, 3, 1, 2, 3, 6))
+
+    def test_get_previous_completed(self):
+        task = DateSecondTaskOk(datetime.datetime(2000, 3, 1, 2, 3, 7))
+        prev = get_previous_completed(task, 3)
+        self.assertEqual(prev.second, datetime.datetime(2000, 3, 1, 2, 3, 4))
+
+    def test_get_previous_completed_not_found(self):
+        task = DateSecondTaskOk(datetime.datetime(2000, 3, 1, 2, 3))
+        prev = get_previous_completed(task, 2)
+        self.assertEqual(None, prev)
+
+
 class DateIntervalTaskOk(luigi.Task):
     interval = luigi.DateIntervalParameter()