[tor-commits] [stem/master] Add timezone to parsed datetimes

atagar at torproject.org atagar at torproject.org
Wed Oct 27 00:44:01 UTC 2021


commit f8717b4bffd46c8c0ee7147f5b50927737b2b5e1
Author: Steven Engler <opara at cs.georgetown.edu>
Date:   Mon Oct 25 14:46:39 2021 -0400

    Add timezone to parsed datetimes
---
 stem/control.py                         |  3 ++-
 stem/descriptor/__init__.py             |  3 ++-
 stem/descriptor/extrainfo_descriptor.py |  2 +-
 stem/descriptor/networkstatus.py        |  2 +-
 stem/descriptor/router_status_entry.py  |  3 ++-
 stem/descriptor/tordnsel.py             |  6 +++---
 stem/response/events.py                 |  8 +++++---
 stem/util/str_tools.py                  | 15 +++++++++++----
 8 files changed, 27 insertions(+), 15 deletions(-)

diff --git a/stem/control.py b/stem/control.py
index e162704b..40ca6bed 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -247,6 +247,7 @@ If you're fine with allowing your script to raise exceptions then this can be mo
 import asyncio
 import calendar
 import collections
+import datetime
 import functools
 import inspect
 import io
@@ -1537,7 +1538,7 @@ class Controller(BaseController):
       self.get_info('accounting/bytes-left'),
     )
 
-    interval_end = stem.util.str_tools._parse_timestamp(interval_end)
+    interval_end = stem.util.str_tools._parse_timestamp(interval_end, datetime.timezone.utc)
     used_read, used_written = [int(val) for val in used.split(' ', 1)]
     left_read, left_written = [int(val) for val in left.split(' ', 1)]
 
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index c015d246..d928ff21 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -108,6 +108,7 @@ import base64
 import codecs
 import collections
 import copy
+import datetime
 import hashlib
 import io
 import os
@@ -683,7 +684,7 @@ def _parse_timestamp_line(keyword: str, attribute: str) -> Callable[['stem.descr
     value = _value(keyword, entries)
 
     try:
-      setattr(descriptor, attribute, stem.util.str_tools._parse_timestamp(value))
+      setattr(descriptor, attribute, stem.util.str_tools._parse_timestamp(value, datetime.timezone.utc))
     except ValueError:
       raise ValueError("Timestamp on %s line wasn't parsable: %s %s" % (keyword, keyword, value))
 
diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py
index 8d9cbf30..ad08d361 100644
--- a/stem/descriptor/extrainfo_descriptor.py
+++ b/stem/descriptor/extrainfo_descriptor.py
@@ -239,7 +239,7 @@ def _parse_timestamp_and_interval(keyword: str, content: str) -> Tuple[datetime.
     raise ValueError("%s line's interval wasn't a number: %s" % (keyword, line))
 
   try:
-    timestamp = stem.util.str_tools._parse_timestamp(timestamp_str)
+    timestamp = stem.util.str_tools._parse_timestamp(timestamp_str, datetime.timezone.utc)
     return timestamp, int(interval), remainder
   except ValueError:
     raise ValueError("%s line's timestamp wasn't parsable: %s" % (keyword, line))
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index e8cf90eb..a69f501f 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -1962,7 +1962,7 @@ class BridgeNetworkStatusDocument(NetworkStatusDocument):
       published_line = published_line.split(' ', 1)[1].strip()
 
       try:
-        self.published = stem.util.str_tools._parse_timestamp(published_line)
+        self.published = stem.util.str_tools._parse_timestamp(published_line, datetime.timezone.utc)
       except ValueError:
         if validate:
           raise ValueError("Bridge network status document's 'published' time wasn't parsable: %s" % published_line)
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py
index aa94a703..eed34fac 100644
--- a/stem/descriptor/router_status_entry.py
+++ b/stem/descriptor/router_status_entry.py
@@ -22,6 +22,7 @@ sources...
 """
 
 import binascii
+import datetime
 import io
 
 import stem.exit_policy
@@ -161,7 +162,7 @@ def _parse_r_line(descriptor: 'stem.descriptor.Descriptor', entries: ENTRY_TYPE)
 
   try:
     published = '%s %s' % (r_comp[3], r_comp[4])
-    descriptor.published = stem.util.str_tools._parse_timestamp(published)
+    descriptor.published = stem.util.str_tools._parse_timestamp(published, datetime.timezone.utc)
   except ValueError:
     raise ValueError("Publication time time wasn't parsable: r %s" % value)
 
diff --git a/stem/descriptor/tordnsel.py b/stem/descriptor/tordnsel.py
index 32cf1863..ffb67578 100644
--- a/stem/descriptor/tordnsel.py
+++ b/stem/descriptor/tordnsel.py
@@ -98,13 +98,13 @@ class TorDNSEL(Descriptor):
         self.fingerprint = value
       elif keyword == 'Published':
         try:
-          self.published = stem.util.str_tools._parse_timestamp(value)
+          self.published = stem.util.str_tools._parse_timestamp(value, datetime.timezone.utc)
         except ValueError:
           if validate:
             raise ValueError("Published time wasn't parsable: %s" % value)
       elif keyword == 'LastStatus':
         try:
-          self.last_status = stem.util.str_tools._parse_timestamp(value)
+          self.last_status = stem.util.str_tools._parse_timestamp(value, datetime.timezone.utc)
         except ValueError:
           if validate:
             raise ValueError("LastStatus time wasn't parsable: %s" % value)
@@ -119,7 +119,7 @@ class TorDNSEL(Descriptor):
               raise ValueError('Unexpected block content: %s' % block_content)
 
           try:
-            date = stem.util.str_tools._parse_timestamp(date_str)
+            date = stem.util.str_tools._parse_timestamp(date_str, datetime.timezone.utc)
             self.exit_addresses.append((address, date))
           except ValueError:
             if validate:
diff --git a/stem/response/events.py b/stem/response/events.py
index 2b9b6dfd..a55771cc 100644
--- a/stem/response/events.py
+++ b/stem/response/events.py
@@ -221,12 +221,14 @@ class AddrMapEvent(Event):
         self.expiry = None
       else:
         try:
-          self.expiry = stem.util.str_tools._parse_timestamp(self.expiry)
+          # control-spec: "Expiry is expressed as the local time (rather than
+          # UTC). This is a bug, left in for backward compatibility"
+          self.expiry = stem.util.str_tools._parse_timestamp(self.expiry, None)
         except ValueError:
           raise stem.ProtocolError('Unable to parse date in ADDRMAP event: %s' % self)
 
     if self.utc_expiry is not None:
-      self.utc_expiry = stem.util.str_tools._parse_timestamp(self.utc_expiry)
+      self.utc_expiry = stem.util.str_tools._parse_timestamp(self.utc_expiry, datetime.timezone.utc)
 
     if self.cached is not None:
       if self.cached == 'YES':
@@ -518,7 +520,7 @@ class ClientsSeenEvent(Event):
 
   def _parse(self) -> None:
     if self.start_time is not None:
-      self.start_time = stem.util.str_tools._parse_timestamp(self.start_time)
+      self.start_time = stem.util.str_tools._parse_timestamp(self.start_time, datetime.timezone.utc)
 
     if self.locales is not None:
       locale_to_count = {}
diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py
index a9a34364..0012e96a 100644
--- a/stem/util/str_tools.py
+++ b/stem/util/str_tools.py
@@ -26,7 +26,7 @@ import sys
 import stem.util
 import stem.util.enum
 
-from typing import List, Sequence, Tuple, Union, overload
+from typing import List, Optional, Sequence, Tuple, Union, overload
 
 # label conversion tuples of the form...
 # (bits / bytes / seconds, short label, long label)
@@ -481,7 +481,7 @@ def parse_short_time_label(label: str) -> int:
     raise ValueError('Non-numeric value in time entry: %s' % label)
 
 
-def _parse_timestamp(entry: str) -> datetime.datetime:
+def _parse_timestamp(entry: str, tz: Optional[datetime.timezone]) -> datetime.datetime:
   """
   Parses the date and time that in format like like...
 
@@ -490,6 +490,8 @@ def _parse_timestamp(entry: str) -> datetime.datetime:
     2012-11-08 16:48:41
 
   :param entry: timestamp to be parsed
+  :param tz: timezone of the entry (if **None**, the returned **datetime** will
+    be "naive")
 
   :returns: **datetime** for the time represented by the timestamp
 
@@ -504,7 +506,12 @@ def _parse_timestamp(entry: str) -> datetime.datetime:
   except AttributeError:
     raise ValueError('Expected timestamp in format YYYY-MM-DD HH:MM:ss but got ' + entry)
 
-  return datetime.datetime(time[0], time[1], time[2], time[3], time[4], time[5])
+  dt = datetime.datetime(time[0], time[1], time[2], time[3], time[4], time[5])
+
+  if tz != None:
+    dt.replace(tzinfo=tz)
+
+  return dt
 
 
 def _parse_iso_timestamp(entry: str) -> 'datetime.datetime':
@@ -541,7 +548,7 @@ def _parse_iso_timestamp(entry: str) -> 'datetime.datetime':
   else:
     raise ValueError("timestamp didn't contain delimeter 'T' between date and time")
 
-  timestamp = _parse_timestamp(timestamp_str)
+  timestamp = _parse_timestamp(timestamp_str, datetime.timezone.utc)
   return timestamp + datetime.timedelta(microseconds = int(microseconds))
 
 



More information about the tor-commits mailing list