Repository: apache/flagon-distill

Issue:
feat(LogMagicMethods): Implement magic methods for log class to allow comparison operators
# Problem
We need to be able to compare logs to each other using arithmetic operators (`>,<,==,>=,...`). This is possible by implementing magic methods (or dunder methods, i.e. `__eq__()`, guide [here](https://rszalski.github.io/magicmethods/)). We also need to decide which metric to compare the logs with; most likely we should use timestamp for chronological comparison–but this doesn't exactly make sense for the equals operator (`==`) so we will need to figure out a different metric for that. 

*Note:* timestamp comparison with arithmetic operators becomes possible by default when using PKSUID and comparing on the ID objects. We could go this route by bubbling up the PKSUID comparison magic methods to the Log object itself. This would also allow us to redefine the ‘==’ comparator method to something other than timestamp if we want to.


# Other notes
- `raw` logs have a single time stamp 
- `interval` logs have a start and stop time stamp 
- We can compare two logs using comparison operators to see if one occurred before or after another 
- `Log1 > Log2` iff Log1 min time strictly greater than Log2 max time 
- `log1 < log2` iff Log1 min time strictly less than Log2 max time 
- `log1 == log2` if Ids are identical (this is one option, in theory they could have different fields though if one of them has been mutated)  
- Need to think through whether this is too overloaded because behavior of operator and the underlying field it's using to compare depends on the operator itself 

# Definition of Done
- Should be comparable with each other 
- Should be able to compare logs against a timestamp 
    - `Log1 < datetime.now()` 
- Should be able to check if logs fall within a given interval: 
    - `datetime.datetime(year=2018, month=3, day=20) < log1 < datetime.datetime(year=2018, month=6, day=21)` 
- If you add two or more logs, the resulting object should be a chronologically-ordered segment 
- Should support [string magic methods](https://www.geeksforgeeks.org/dunder-magic-methods-python/)

Discussion:
@jlhitzeman: Assign

Solution patch:
diff --git a/distill/core/log.py b/distill/core/log.py
index e8c1f7a..e114a18 100644
--- a/distill/core/log.py
+++ b/distill/core/log.py
@@ -17,6 +17,7 @@ import json
 from typing import Dict, Union
 
 from pksuid import PKSUID
+from datetime import datetime
 from pydantic import BaseModel, parse_obj_as
 from pydantic.type_adapter import TypeAdapter
 
@@ -60,3 +61,37 @@ class Log:
 
     def to_dict(self) -> JsonDict:
         return self.data.model_dump(by_alias=True)
+    
+    def __lt__(self, other):
+        if isinstance(other, Log):
+            return self.id < other.id
+        if isinstance(other, datetime):
+            return self.id.get_datetime() < other
+    
+    def __le__(self, other):
+        if isinstance(other, Log):
+            return self.id <= other.id
+        if isinstance(other, datetime):
+            return self.id.get_datetime() <= other
+    
+    def __gt__(self, other):
+        if isinstance(other, Log):
+            return self.id > other.id
+        if isinstance(other, datetime):
+            return self.id.get_datetime() > other
+    
+    def __ge__(self, other):
+        if isinstance(other, Log):
+            return self.id > other.id
+        if isinstance(other, datetime):
+            return self.id.get_datetime() >= other
+    
+    def __ne__(self, other):
+        if isinstance(other, Log):
+            return self.data != other.data
+        return NotImplemented
+    
+    def __eq__(self, other):
+        if isinstance(other, Log):
+            return self.data == other.data
+        return NotImplemented


Test patch:
diff --git a/tests/test_log.py b/tests/test_log.py
index fc68de0..7b0a4a4 100644
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -91,7 +91,20 @@ def test_log_normalize_timestamp():
     assert test_log.data.client_time == correct_dt
     assert test_log.to_dict()["clientTime"] == correct_ms
 
+def test_log_comparison():
+    data = load_log()
+    test_log = Log(data=data)
+
+    test_log_equal = Log(data=data)
+
+    data_other = json.loads(load_log())
+    data_other["clientTime"] += 10000
+    data_other = json.dumps(data_other)
+    test_other = Log(data=data_other)
 
+    assert test_log == test_log_equal
+    assert test_log < test_other < datetime.now()
+    
 def load_log() -> str:
     with open(os.path.join(DATA_DIR, "log_test_data.json")) as f:
         data = f.readline()