Repository: google/timesketch

Issue:
[API Client] Update search module to support millisecond dates
**Describe the bug**
The new UI now allows date chips with millisecond precision.  The API client needs to be updated to support these values.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to a sketch
2. Create a saved search with sub-second precision
3. Save the search
4. Use the timesketch CLI as follows `timesketch --sketch <sketch_id> saved-searches describe <saved-search-id>`.  This will raise a ValueError that "Microsecond dates are not currently supported".

**Expected behavior**
The timesketch CLI does not produce an error and lists the saved search information.


Discussion:

Patch:
diff --git a/api_client/python/timesketch_api_client/search.py b/api_client/python/timesketch_api_client/search.py
index 23b85486..a7c9a861 100644
--- a/api_client/python/timesketch_api_client/search.py
+++ b/api_client/python/timesketch_api_client/search.py
@@ -158,7 +158,9 @@ class DateIntervalChip(Chip):
         """Property that returns back the date."""
         if not self._date:
             return ""
-        return self._date.strftime(self._DATE_FORMAT)
+        if self._date.microsecond == 0:
+            return self._date.strftime(self._DATE_FORMAT)
+        return self._date.strftime(self._DATE_FORMAT_MICROSECONDS)[:-3]
 
     @date.setter
     def date(self, date):
@@ -176,8 +178,6 @@ class DateIntervalChip(Chip):
                         "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):
@@ -268,8 +268,6 @@ class DateRangeChip(Chip):
                     "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):
@@ -298,8 +296,6 @@ class DateRangeChip(Chip):
                     "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
@@ -307,7 +303,9 @@ class DateRangeChip(Chip):
         """Property that returns the end time of a range."""
         if not self._end_date:
             return ""
-        return self._end_date.strftime(self._DATE_FORMAT)
+        if self._end_date.microsecond == 0:
+            return self._end_date.strftime(self._DATE_FORMAT)
+        return self._end_date.strftime(self._DATE_FORMAT_MICROSECONDS)[:-3]
 
     @end_time.setter
     def end_time(self, end_time):
@@ -340,7 +338,9 @@ class DateRangeChip(Chip):
         """Property that returns the start time of a range."""
         if not self._start_date:
             return ""
-        return self._start_date.strftime(self._DATE_FORMAT)
+        if self._start_date.microsecond == 0:
+            return self._start_date.strftime(self._DATE_FORMAT)
+        return self._start_date.strftime(self._DATE_FORMAT_MICROSECONDS)[:-3]
 
     @start_time.setter
     def start_time(self, start_time):


Test patch:
diff --git a/api_client/python/timesketch_api_client/search_test.py b/api_client/python/timesketch_api_client/search_test.py
index 2291b7a2..8695253d 100644
--- a/api_client/python/timesketch_api_client/search_test.py
+++ b/api_client/python/timesketch_api_client/search_test.py
@@ -110,6 +110,7 @@ class SearchTest(unittest.TestCase):
         with self.assertRaises(ValueError):
             chip.date = "20 minutes"
 
+        chip = search.DateIntervalChip()
         date_string = "2020-11-30T12:12:12"
         chip.date = date_string
 
@@ -147,6 +148,7 @@ class SearchTest(unittest.TestCase):
         with self.assertRaises(ValueError):
             chip.start_time = "20 minutes"
 
+        chip = search.DateRangeChip()
         date_string = "2020-12-12T12:12:12,2020-12-12T12:12:12"
         chip.from_dict({"value": date_string})
 
@@ -178,11 +180,21 @@ class SearchTest(unittest.TestCase):
         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})
+        chip_micro = search.DateRangeChip()
+        date_string = "2020-12-12T12:12:12.001,2020-12-12T12:12:12.002"
+        chip_micro.from_dict({"value": date_string})
+
+        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.001")
+        self.assertEqual(chip_micro.end_time, "2020-12-12T12:12:12.002")
 
     def test_from_date_interval(self):
         """Test from_date method in DateIntervalChip."""
@@ -251,6 +263,19 @@ class SearchTest(unittest.TestCase):
         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})
+        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.001 -1m +1m",
+        }
+
+        self.assertEqual(chip.date, "2021-11-30T12:12:12.001")
+        self.assertEqual(chip.before, 1)
+        self.assertEqual(chip.after, 1)
+        self.assertEqual(chip.unit, "m")
+        self.assertEqual(chip.chip, expected_chip)