[tor-commits] [stem/master] Add timestamp to CIRC_BW and STREAM_BW events

atagar at torproject.org atagar at torproject.org
Fri Sep 15 19:02:11 UTC 2017


commit 975bc0645e5a79ceeac31f4f2acdc289fc463480
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri Sep 15 12:01:00 2017 -0700

    Add timestamp to CIRC_BW and STREAM_BW events
    
    Supporting new attributes...
    
      https://gitweb.torproject.org/torspec.git/commit/?id=00b9daf
---
 docs/change_log.rst          |  7 ++++---
 stem/response/events.py      | 46 +++++++++++++++++++++++++++++++-------------
 stem/util/str_tools.py       |  2 +-
 test/unit/response/events.py | 19 +++++++++++++++++-
 test/unit/util/str_tools.py  |  1 +
 5 files changed, 57 insertions(+), 18 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index dafdb471..60457e48 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -55,6 +55,7 @@ The following are only available within Stem's `git repository
   * Tor change caused :func:`~stem.control.Controller.list_ephemeral_hidden_services` to provide empty strings if unset (:trac:`21329`)
   * Better error message when :func:`~stem.control.Controller.set_conf` fails due to an option being immutable
   * :func:`~stem.control.Controller.is_geoip_unavailable` now determines if database is available right away
+  * Added the time attribute to :class:`~stem.response.events.StreamBwEvent` and :class:`~stem.response.events.CircuitBandwidthEvent` (:spec:`00b9daf`)
   * Deprecated :func:`~stem.control.Controller.is_geoip_unavailable`, this is now available via getinfo instead (:trac:`23237`, :spec:`dc973f8`)
   * Deprecated :class:`~stem.respose.events.AuthDirNewDescEvent` (:trac:`22377`, :spec:`6e887ba`)
   * Caching manual information as sqlite rather than stem.util.conf, making :func:`stem.manual.Manual.from_cache` about ~8x faster
@@ -119,7 +120,7 @@ and much more.
   * Added :func:`~stem.control.Controller.reconnect` to the :class:`~stem.control.Controller`
   * Added :func:`~stem.control.Controller.is_set` to the :class:`~stem.control.Controller`
   * Added :func:`~stem.control.Controller.is_user_traffic_allowed` to the :class:`~stem.control.Controller`
-  * Added the replica attribute to the :class:`~stem.response.events.HSDescEvent` (:spec:`4989e73`)
+  * Added the replica attribute to :class:`~stem.response.events.HSDescEvent` (:spec:`4989e73`)
   * Added the NoEdConsensus :data:`~stem.Flag` (:spec:`dc99160`)
   * Recognize listeners with IPv6 addresses in :func:`~stem.control.Controller.get_listeners`
   * :func:`~stem.process.launch_tor` could leave a lingering process during an unexpected exception (:trac:`17946`)
@@ -255,7 +256,7 @@ brings see `Nathan Willis' LWN article <http://lwn.net/Articles/632914/>`_.
   * Added :func:`~stem.control.BaseController.connection_time` to the :class:`~stem.control.BaseController`
   * Changed :func:`~stem.control.Controller.get_microdescriptor`, :func:`~stem.control.Controller.get_server_descriptor`, and :func:`~stem.control.Controller.get_network_status` to get our own descriptor if no fingerprint or nickname is provided.
   * Added :class:`~stem.exit_policy.ExitPolicy` methods for more easily handling 'private' policies (the `default prefix <https://www.torproject.org/docs/tor-manual.html.en#ExitPolicyRejectPrivate>`_) and the defaultly appended suffix. This includes :func:`~stem.exit_policy.ExitPolicy.has_private`, :func:`~stem.exit_policy.ExitPolicy.strip_private`, :func:`~stem.exit_policy.ExitPolicy.has_default`, and :func:`~stem.exit_policy.ExitPolicy.strip_default` :class:`~stem.exit_policy.ExitPolicy` methods in addition to :func:`~stem.exit_policy.ExitPolicyRule.is_private` and :func:`~stem.exit_policy.ExitPolicyRule.is_default` for the :class:`~stem.exit_policy.ExitPolicyRule`. (:trac:`10107`)
-  * Added the reason attribute to the :class:`~stem.response.events.HSDescEvent` (:spec:`7908c8d`)
+  * Added the reason attribute to :class:`~stem.response.events.HSDescEvent` (:spec:`7908c8d`)
   * :func:`~stem.process.launch_tor_with_config` could cause a "Too many open files" OSError if called too many times (:trac:`13141`)
   * The :func:`~stem.control.Controller.get_exit_policy` method errored if tor couldn't determine our external address
   * The Controller's methods for retrieving descriptors could raise unexpected ValueErrors if tor didn't have any descriptors available
@@ -305,7 +306,7 @@ among numerous other improvements and fixes.
   * Added :func:`~stem.control.Controller.is_newnym_available` and :func:`~stem.control.Controller.get_newnym_wait` methods to the :class:`~stem.control.Controller`
   * Added :func:`~stem.control.Controller.get_ports` and :func:`~stem.control.Controller.get_listeners` methods to the :class:`~stem.control.Controller`
   * Added :func:`~stem.control.Controller.drop_guards` (:trac:`10032`, :spec:`7c6c7fc`)
-  * Added the id attribute to the :class:`~stem.response.events.ORConnEvent` (:spec:`6f2919a`)
+  * Added the id attribute to :class:`~stem.response.events.ORConnEvent` (:spec:`6f2919a`)
   * Added `support for CONN_BW events <api/response.html#stem.response.events.ConnectionBandwidthEvent>`_ (:spec:`6f2919a`)
   * Added `support for CIRC_BW events <api/response.html#stem.response.events.CircuitBandwidthEvent>`_ (:spec:`6f2919a`)
   * Added `support for CELL_STATS events <api/response.html#stem.response.events.CellStatsEvent>`_ (:spec:`6f2919a`)
diff --git a/stem/response/events.py b/stem/response/events.py
index 45e4677b..8392250b 100644
--- a/stem/response/events.py
+++ b/stem/response/events.py
@@ -125,6 +125,25 @@ class Event(stem.response.ControlMessage):
     for controller_attr_name, attr_name in self._KEYWORD_ARGS.items():
       setattr(self, attr_name, self.keyword_args.get(controller_attr_name))
 
+  def _iso_timestamp(self, timestamp):
+    """
+    Parses an iso timestamp (ISOTime2Frac in the control-spec).
+
+    :param str timestamp: timestamp to parse
+
+    :returns: **datetime** with the parsed timestamp
+
+    :raises: :class:`stem.ProtocolError` if timestamp is malformed
+    """
+
+    if timestamp is None:
+      return None
+
+    try:
+      return str_tools._parse_iso_timestamp(timestamp)
+    except ValueError as exc:
+      raise stem.ProtocolError('Unable to parse timestamp (%s): %s' % (exc, self))
+
   # method overwritten by our subclasses for special handling that they do
   def _parse(self):
     pass
@@ -369,16 +388,11 @@ class CircuitEvent(Event):
 
   def _parse(self):
     self.path = tuple(stem.control._parse_circ_path(self.path))
+    self.created = self._iso_timestamp(self.created)
 
     if self.build_flags is not None:
       self.build_flags = tuple(self.build_flags.split(','))
 
-    if self.created is not None:
-      try:
-        self.created = str_tools._parse_iso_timestamp(self.created)
-      except ValueError as exc:
-        raise stem.ProtocolError('Unable to parse create date (%s): %s' % (exc, self))
-
     if not tor_tools.is_valid_circuit_id(self.id):
       raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self))
 
@@ -468,16 +482,11 @@ class CircMinorEvent(Event):
 
   def _parse(self):
     self.path = tuple(stem.control._parse_circ_path(self.path))
+    self.created = self._iso_timestamp(self.created)
 
     if self.build_flags is not None:
       self.build_flags = tuple(self.build_flags.split(','))
 
-    if self.created is not None:
-      try:
-        self.created = str_tools._parse_iso_timestamp(self.created)
-      except ValueError as exc:
-        raise stem.ProtocolError('Unable to parse create date (%s): %s' % (exc, self))
-
     if not tor_tools.is_valid_circuit_id(self.id):
       raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self))
 
@@ -1055,12 +1064,16 @@ class StreamBwEvent(Event):
 
   The STREAM_BW event was introduced in tor version 0.1.2.8-beta.
 
+  .. versionchanged:: 1.6.0
+     Added the time attribute.
+
   :var str id: stream identifier
   :var long written: bytes sent by the application
   :var long read: bytes received by the application
+  :var datetime time: time when the measurement was recorded
   """
 
-  _POSITIONAL_ARGS = ('id', 'written', 'read')
+  _POSITIONAL_ARGS = ('id', 'written', 'read', 'time')
   _VERSION_ADDED = stem.version.Requirement.EVENT_STREAM_BW
 
   def _parse(self):
@@ -1075,6 +1088,7 @@ class StreamBwEvent(Event):
 
     self.read = int_type(self.read)
     self.written = int_type(self.written)
+    self.time = self._iso_timestamp(self.time)
 
 
 class TransportLaunchedEvent(Event):
@@ -1166,15 +1180,20 @@ class CircuitBandwidthEvent(Event):
 
   .. versionadded:: 1.2.0
 
+  .. versionchanged:: 1.6.0
+     Added the time attribute.
+
   :var str id: circuit identifier
   :var long read: bytes received by tor that second
   :var long written: bytes sent by tor that second
+  :var datetime time: time when the measurement was recorded
   """
 
   _KEYWORD_ARGS = {
     'ID': 'id',
     'READ': 'read',
     'WRITTEN': 'written',
+    'TIME': 'time',
   }
 
   _VERSION_ADDED = stem.version.Requirement.EVENT_CIRC_BW
@@ -1193,6 +1212,7 @@ class CircuitBandwidthEvent(Event):
 
     self.read = int_type(self.read)
     self.written = int_type(self.written)
+    self.time = self._iso_timestamp(self.time)
 
 
 class CellStatsEvent(Event):
diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py
index 14f8a7cf..bc0446fa 100644
--- a/stem/util/str_tools.py
+++ b/stem/util/str_tools.py
@@ -537,7 +537,7 @@ def _parse_iso_timestamp(entry):
   if len(microseconds) != 6 or not microseconds.isdigit():
     raise ValueError("timestamp's microseconds should be six digits")
 
-  if timestamp_str[10] == 'T':
+  if len(timestamp_str) > 10 and timestamp_str[10] == 'T':
     timestamp_str = timestamp_str[:10] + ' ' + timestamp_str[11:]
   else:
     raise ValueError("timestamp didn't contain delimeter 'T' between date and time")
diff --git a/test/unit/response/events.py b/test/unit/response/events.py
index ae7fd606..87f3e329 100644
--- a/test/unit/response/events.py
+++ b/test/unit/response/events.py
@@ -487,8 +487,10 @@ CONN_BW_BAD_WRITTEN_VALUE = '650 CONN_BW ID=11 TYPE=DIR READ=272 WRITTEN=817.7'
 CONN_BW_BAD_MISSING_ID = '650 CONN_BW TYPE=DIR READ=272 WRITTEN=817'
 
 CIRC_BW = '650 CIRC_BW ID=11 READ=272 WRITTEN=817'
+CIRC_BW_WITH_TIMESTAMP = '650 CIRC_BW ID=11 READ=272 WRITTEN=817 TIME=2012-12-06T13:51:11.433755'
 CIRC_BW_BAD_WRITTEN_VALUE = '650 CIRC_BW ID=11 READ=272 WRITTEN=817.7'
 CIRC_BW_BAD_MISSING_ID = '650 CIRC_BW READ=272 WRITTEN=817'
+CIRC_BW_MALFORMED_TIMESTAMP = '650 CIRC_BW ID=11 READ=272 WRITTEN=817 TIME=boom'
 
 CELL_STATS_1 = '650 CELL_STATS ID=14 \
 OutboundQueue=19403 OutboundConn=15 \
@@ -1457,11 +1459,19 @@ class TestEvents(unittest.TestCase):
     self.assertEqual('2', event.id)
     self.assertEqual(15, event.written)
     self.assertEqual(25, event.read)
+    self.assertEqual(None, event.time)
 
     event = _get_event('650 STREAM_BW Stream02 0 0')
     self.assertEqual('Stream02', event.id)
     self.assertEqual(0, event.written)
     self.assertEqual(0, event.read)
+    self.assertEqual(None, event.time)
+
+    event = _get_event('650 STREAM_BW Stream02 0 0 2012-12-06T13:51:11.433755')
+    self.assertEqual('Stream02', event.id)
+    self.assertEqual(0, event.written)
+    self.assertEqual(0, event.read)
+    self.assertEqual(datetime.datetime(2012, 12, 6, 13, 51, 11, 433755), event.time)
 
     self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW')
     self.assertRaises(ProtocolError, _get_event, '650 STREAM_BW 2')
@@ -1501,15 +1511,22 @@ class TestEvents(unittest.TestCase):
 
   def test_circ_bw_event(self):
     event = _get_event(CIRC_BW)
-
     self.assertTrue(isinstance(event, stem.response.events.CircuitBandwidthEvent))
     self.assertEqual(CIRC_BW.lstrip('650 '), str(event))
     self.assertEqual('11', event.id)
     self.assertEqual(272, event.read)
     self.assertEqual(817, event.written)
+    self.assertEqual(None, event.time)
+
+    event = _get_event(CIRC_BW_WITH_TIMESTAMP)
+    self.assertEqual('11', event.id)
+    self.assertEqual(272, event.read)
+    self.assertEqual(817, event.written)
+    self.assertEqual(datetime.datetime(2012, 12, 6, 13, 51, 11, 433755), event.time)
 
     self.assertRaises(ProtocolError, _get_event, CIRC_BW_BAD_WRITTEN_VALUE)
     self.assertRaises(ProtocolError, _get_event, CIRC_BW_BAD_MISSING_ID)
+    self.assertRaises(ProtocolError, _get_event, CIRC_BW_MALFORMED_TIMESTAMP)
 
   def test_cell_stats_event(self):
     event = _get_event(CELL_STATS_1)
diff --git a/test/unit/util/str_tools.py b/test/unit/util/str_tools.py
index 979f0896..ee270717 100644
--- a/test/unit/util/str_tools.py
+++ b/test/unit/util/str_tools.py
@@ -180,6 +180,7 @@ class TestStrTools(unittest.TestCase):
     invalid_input = [
       None,
       32,
+      'boom',
       'hello world',
       '2012-11-08T16:48:41.42025',    # too few microsecond digits
       '2012-11-08T16:48:41.4202511',  # too many microsecond digits



More information about the tor-commits mailing list