commit 975bc0645e5a79ceeac31f4f2acdc289fc463480 Author: Damian Johnson atagar@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