commit b82c89ebc55f6d9fb54dfa93afd03af6e34980dd Author: Damian Johnson atagar@torproject.org Date: Sun Sep 14 14:40:22 2014 -0700
Adding get_accounting_stats()
Controller method for fetching our accounting stats. Much nicer than calling getinfo() three times and parsing the results ourselves. --- docs/change_log.rst | 1 + stem/control.py | 73 +++++++++++++++++++++++++++++++++++++++ test/unit/control/controller.py | 30 ++++++++++++++++ 3 files changed, 104 insertions(+)
diff --git a/docs/change_log.rst b/docs/change_log.rst index ec7eaef..ff58d12 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -42,6 +42,7 @@ The following are only available within Stem's `git repository
* **Controller**
+ * Added :func:`~stem.control.Controller.get_accounting_stats` to the :class:`~stem.control.Controller` * 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`) diff --git a/stem/control.py b/stem/control.py index 3c26666..57e4550 100644 --- a/stem/control.py +++ b/stem/control.py @@ -77,6 +77,7 @@ If you're fine with allowing your script to raise exceptions then this can be mo |- get_exit_policy - provides our exit policy |- get_ports - provides the local ports where tor is listening for connections |- get_listeners - provides the addresses and ports where tor is listening for connections + |- get_accounting_stats - provides stats related to relaying limits |- get_protocolinfo - information about the controller interface |- get_user - provides the user tor is running as |- get_pid - provides the pid of our tor process @@ -212,6 +213,9 @@ If you're fine with allowing your script to raise exceptions then this can be mo ============= =========== """
+import calendar +import collections +import datetime import io import os import Queue @@ -327,6 +331,19 @@ SERVER_DESCRIPTORS_UNSUPPORTED = "Tor is presently not configured to retrieve \ server descriptors. As of Tor version 0.2.3.25 it downloads microdescriptors \ instead unless you set 'UseMicrodescriptors 0' in your torrc."
+AccountingStats = collections.namedtuple('AccountingStats', [ + 'retrieved', + 'status', + 'interval_end', + 'time_until_reset', + 'read_bytes', + 'read_bytes_left', + 'read_limit', + 'written_bytes', + 'write_bytes_left', + 'write_limit', +]) +
class BaseController(object): """ @@ -1183,6 +1200,62 @@ class Controller(BaseController): else: return default
+ def get_accounting_stats(self, default = UNDEFINED): + """ + Provides stats related to our relaying limitations if AccountingMax was set + in our torrc. This provides a **namedtuple** with the following + attributes... + + * retrieved (float) - unix timestamp for when this was fetched + * status (str) - hibernation status of 'awake', 'soft', or 'hard' + * interval_end (datetime) + * time_until_reset (int) - seconds until our limits reset + * read_bytes (int) + * read_bytes_left (int) + * read_limit (int) + * written_bytes (int) + * write_bytes_left (int) + * write_limit (int) + + .. versionadded:: 1.3.0 + + :param object default: response if the query fails + + :returns: **namedtuple** with our accounting stats + + :raises: :class:`stem.ControllerError` if unable to determine the listeners + and no default was provided + """ + + try: + retrieved = time.time() + status = self.get_info('accounting/hibernating') + interval_end = self.get_info('accounting/interval-end') + used = self.get_info('accounting/bytes') + left = self.get_info('accounting/bytes-left') + + interval_end = datetime.datetime.strptime(interval_end, '%Y-%m-%d %H:%M:%S') + used_read, used_written = [int(val) for val in used.split(' ', 1)] + left_read, left_written = [int(val) for val in left.split(' ', 1)] + + return AccountingStats( + retrieved = retrieved, + status = status, + interval_end = interval_end, + time_until_reset = int(retrieved) - calendar.timegm(interval_end.timetuple()), + read_bytes = used_read, + read_bytes_left = left_read, + read_limit = used_read + left_read, + written_bytes = used_written, + write_bytes_left = left_written, + write_limit = used_written + left_written, + ) + except Exception as exc: + if default == UNDEFINED: + raise exc + else: + return default + def get_socks_listeners(self, default = UNDEFINED): """ Provides the SOCKS **(address, port)** tuples that tor has open. diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py index 17b64fd..4e04f9d 100644 --- a/test/unit/control/controller.py +++ b/test/unit/control/controller.py @@ -3,6 +3,7 @@ Unit tests for the stem.control module. The module's primarily exercised via integ tests, but a few bits lend themselves to unit testing. """
+import datetime import io import unittest
@@ -279,6 +280,35 @@ class TestControl(unittest.TestCase): get_info_mock.return_value = response self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
+ @patch('stem.control.Controller.get_info') + @patch('time.time', Mock(return_value = 1410723698.276578)) + def test_get_accounting_stats(self, get_info_mock): + """ + Exercises the get_accounting_stats() method. + """ + + get_info_mock.side_effect = lambda param, **kwargs: { + 'accounting/hibernating': 'awake', + 'accounting/interval-end': '2014-09-14 19:41:00', + 'accounting/bytes': '4837 2050', + 'accounting/bytes-left': '102944 7440', + }[param] + + expected = stem.control.AccountingStats( + 1410723698.276578, + 'awake', + datetime.datetime(2014, 9, 14, 19, 41), + 38, + 4837, 102944, 107781, + 2050, 7440, 9490, + ) + + self.assertEqual(expected, self.controller.get_accounting_stats()) + + get_info_mock.side_effect = ControllerError('nope, too bad') + self.assertRaises(ControllerError, self.controller.get_accounting_stats) + self.assertEqual('my default', self.controller.get_accounting_stats('my default')) + @patch('stem.connection.get_protocolinfo') def test_get_protocolinfo(self, get_protocolinfo_mock): """