Repository: google/timesketch

Issue:
ValueError in date method in DateIntervalChip 
**Describe the bug**
A date chip value in the following format ```"value": "YYYY-MM-DD -1d +1d" ``` raises a ValueError which is caught as expected, see below: 

https://github.com/google/timesketch/blob/5a0040531e979022c91017c2c641273dc1ec1183/api_client/python/timesketch_api_client/search.py#L163-L165

Because of this, views with this particular type of date format are being ignored/skipped when calling ```sketch.list_views()```.




Discussion:
Another observed value "YYYY-MM-DD HH:MM:SS -1m +1m" causes a "ValueError: too many values to unpack" on this line:

https://github.com/google/timesketch/blob/5a0040531e979022c91017c2c641273dc1ec1183/api_client/python/timesketch_api_client/search.py#L176

Resolving patch:
diff --git a/api_client/python/timesketch_api_client/search.py b/api_client/python/timesketch_api_client/search.py
--- a/api_client/python/timesketch_api_client/search.py
+++ b/api_client/python/timesketch_api_client/search.py
@@ -98,6 +98,8 @@ class DateIntervalChip(Chip):
     CHIP_VALUE = 'interval'
 
     _DATE_FORMAT = '%Y-%m-%dT%H:%M:%S'
+    _DATE_FORMAT_MICROSECONDS = '%Y-%m-%dT%H:%M:%S.%f'
+    _DATE_ONLY_FORMAT = '%Y-%m-%d'
 
     def __init__(self):
         """Initialize the chip."""
@@ -161,11 +163,22 @@ def date(self):
     def date(self, date):
         """Make changes to the date."""
         try:
-            dt = datetime.datetime.strptime(date, self._DATE_FORMAT)
-        except ValueError as exc:
-            logger.error(
-                'Unable to add date chip, wrong date format', exc_info=True)
-            raise ValueError('Wrong date format') from exc
+            dt = datetime.datetime.strptime(date,
+                                            self._DATE_FORMAT_MICROSECONDS)
+        except ValueError:
+            try:
+                dt = datetime.datetime.strptime(date, self._DATE_FORMAT)
+            except ValueError:
+                try:
+                    dt = datetime.datetime.strptime(date,
+                                                    self._DATE_ONLY_FORMAT)
+                except ValueError as exc:
+                    logger.error(
+                        'Unable to add date chip, wrong date format',
+                        exc_info=True)
+                    raise ValueError('Wrong date format') from exc
+        if dt.microsecond > 0:
+            raise ValueError('Microsecond dates are not currently supported')
         self._date = dt
 
     def from_dict(self, chip_dict):
@@ -173,9 +186,22 @@ def from_dict(self, chip_dict):
         value = chip_dict.get('value')
         if not value:
             return
-        date, before, after = value.split()
+
+        split_value = value.split()
+        if len(split_value) == 3:
+            date_time, before, after = split_value
+        elif len(split_value) == 4:
+            date, time, before, after = split_value
+            date_time = f'{date}T{time}'
+        else:
+            raise ValueError(
+                'Unable to configure date chip, wrong date format.')
+
         self.unit = before[-1]
-        self.date = date
+        if date_time.endswith('Z'):
+            self.date = date_time[:-1]
+        else:
+            self.date = date_time
         self.before = int(before[1:-1])
         self.after = int(after[1:-1])
 
@@ -207,6 +233,7 @@ class DateRangeChip(Chip):
     CHIP_VALUE = 'date_range'
 
     _DATE_FORMAT = '%Y-%m-%dT%H:%M:%S'
+    _DATE_FORMAT_MICROSECONDS = '%Y-%m-%dT%H:%M:%S.%f'
 
     def __init__(self):
         """Initialize the date range."""
@@ -223,12 +250,21 @@ def add_end_time(self, end_time):
         Raises:
             ValueError: if the date format is incorrectly formatted.
         """
+        if end_time.endswith('Z'):
+            end_time = end_time[:-1]
         try:
-            dt = datetime.datetime.strptime(end_time, self._DATE_FORMAT)
+            dt = datetime.datetime.strptime(end_time,
+                                            self._DATE_FORMAT_MICROSECONDS)
         except ValueError as exc:
-            logger.error(
-                'Unable to add date chip, wrong date format', exc_info=True)
-            raise ValueError('Wrong date format') from exc
+            try:
+                dt = datetime.datetime.strptime(end_time, self._DATE_FORMAT)
+            except ValueError as exc:
+                logger.error(
+                    'Unable to add date chip, wrong date format',
+                    exc_info=True)
+                raise ValueError('Wrong date format') from exc
+        if dt.microsecond > 0:
+            raise ValueError('Microsecond dates are not currently supported')
         self._end_date = dt
 
     def add_start_time(self, start_time):
@@ -240,12 +276,21 @@ def add_start_time(self, start_time):
         Raises:
             ValueError: if the date format is incorrectly formatted.
         """
+        if start_time.endswith('Z'):
+            start_time = start_time[:-1]
         try:
-            dt = datetime.datetime.strptime(start_time, self._DATE_FORMAT)
+            dt = datetime.datetime.strptime(start_time,
+                                            self._DATE_FORMAT_MICROSECONDS)
         except ValueError as exc:
-            logger.error(
-                'Unable to add date chip, wrong date format', exc_info=True)
-            raise ValueError('Wrong date format') from exc
+            try:
+                dt = datetime.datetime.strptime(start_time, self._DATE_FORMAT)
+            except ValueError as exc:
+                logger.error(
+                    'Unable to add date chip, wrong date format',
+                    exc_info=True)
+                raise ValueError('Wrong date format') from exc
+        if dt.microsecond > 0:
+            raise ValueError('Microsecond dates are not currently supported')
         self._start_date = dt
 
     @property


Test patch:
diff --git a/api_client/python/timesketch_api_client/search_test.py b/api_client/python/timesketch_api_client/search_test.py
--- a/api_client/python/timesketch_api_client/search_test.py
+++ b/api_client/python/timesketch_api_client/search_test.py
@@ -130,3 +130,130 @@ def test_date_interval(self):
         expected_chip['value'] = f'{date_string} -1h +6h'
 
         self.assertEqual(chip.chip, expected_chip)
+
+        alt_date_string = '2021-11-30'
+        alt_chip = search.DateIntervalChip()
+        alt_chip.date = alt_date_string
+        alt_expected_chip = {
+            'active': True,
+            'field': '',
+            'type': 'datetime_interval',
+            'operator': 'must',
+            'value': f'{alt_date_string}T00:00:00 -5m +5m'
+        }
+        self.assertEqual(alt_chip.chip, alt_expected_chip)
+
+    def test_date_range(self):
+        """Test date range chip."""
+        chip = search.DateRangeChip()
+        with self.assertRaises(ValueError):
+            chip.start_time = '20 minutes'
+
+        date_string = '2020-12-12T12:12:12,2020-12-12T12:12:12'
+        chip.from_dict({'value': date_string})
+
+        expected_chip = {
+            'active': True,
+            'field': '',
+            'type': 'datetime_range',
+            'operator': 'must',
+            'value': date_string
+        }
+
+        self.assertEqual(chip.chip, expected_chip)
+        self.assertEqual(chip.start_time, '2020-12-12T12:12:12')
+        self.assertEqual(chip.end_time, '2020-12-12T12:12:12')
+
+        chip_micro = search.DateRangeChip()
+        date_string_micro = '2020-12-12T12:12:12.000Z,2020-12-12T12:12:12.000Z'
+        chip_micro.from_dict({'value': date_string_micro})
+
+        expected_chip = {
+            'active': True,
+            'field': '',
+            'type': 'datetime_range',
+            'operator': 'must',
+            'value': date_string
+        }
+
+        self.assertEqual(chip_micro.chip, expected_chip)
+        self.assertEqual(chip_micro.start_time, '2020-12-12T12:12:12')
+        self.assertEqual(chip_micro.end_time, '2020-12-12T12:12:12')
+
+        chip = search.DateRangeChip()
+        with self.assertRaises(ValueError):
+            chip = search.DateRangeChip()
+            date_string = '2020-12-12T12:12:12.001,2020-12-12T12:12:12.001'
+            chip.from_dict({'value': date_string})
+
+
+    def test_from_date_interval(self):
+        """Test from_date method in DateIntervalChip."""
+        date_string = "2021-11-30T12:12:12 -1m +1m"
+        chip = search.DateIntervalChip()
+        chip.from_dict({'value': date_string})
+
+        expected_chip = {
+            'active': True,
+            'field': '',
+            'type': 'datetime_interval',
+            'operator': 'must',
+            'value': date_string
+        }
+
+        self.assertEqual(chip.date, "2021-11-30T12:12:12")
+        self.assertEqual(chip.before, 1)
+        self.assertEqual(chip.after, 1)
+        self.assertEqual(chip.unit, 'm')
+        self.assertEqual(chip.chip, expected_chip)
+
+        date_string = "2021-11-30 12:12:12 -1m +1m"
+        chip = search.DateIntervalChip()
+        chip.from_dict({'value': date_string})
+
+        self.assertEqual(chip.date, "2021-11-30T12:12:12")
+        self.assertEqual(chip.before, 1)
+        self.assertEqual(chip.after, 1)
+        self.assertEqual(chip.unit, 'm')
+        self.assertEqual(chip.chip, expected_chip)
+
+        date_string = "2021-11-30 -1d +1d"
+        chip = search.DateIntervalChip()
+        chip.from_dict({'value': date_string})
+
+        expected_chip = {
+            'active': True,
+            'field': '',
+            'type': 'datetime_interval',
+            'operator': 'must',
+            'value': "2021-11-30T00:00:00 -1d +1d"
+        }
+
+        self.assertEqual(chip.date, "2021-11-30T00:00:00")
+        self.assertEqual(chip.before, 1)
+        self.assertEqual(chip.after, 1)
+        self.assertEqual(chip.unit, 'd')
+        self.assertEqual(chip.chip, expected_chip)
+
+        date_string = "2021-11-30T12:12:12.000Z -1m +1m"
+        chip = search.DateIntervalChip()
+        chip.from_dict({'value': date_string})
+
+        expected_chip = {
+            'active': True,
+            'field': '',
+            'type': 'datetime_interval',
+            'operator': 'must',
+            'value': "2021-11-30T12:12:12 -1m +1m"
+        }
+
+        self.assertEqual(chip.date, "2021-11-30T12:12:12")
+        self.assertEqual(chip.before, 1)
+        self.assertEqual(chip.after, 1)
+        self.assertEqual(chip.unit, 'm')
+        self.assertEqual(chip.chip, expected_chip)
+
+        date_string = "2021-11-30T12:12:12.001Z -1m +1m"
+        with self.assertRaises(ValueError):
+            chip = search.DateIntervalChip()
+            chip.from_dict({'value': date_string})