File: tortoise_and_the_hare.rst

package info (click to toggle)
python-stem 1.2.2-1.1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 4,568 kB
  • ctags: 2,036
  • sloc: python: 20,108; makefile: 127; sh: 3
file content (211 lines) | stat: -rw-r--r-- 6,840 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
Tortoise and the Hare
=====================

Controllers have two methods of talking with Tor...

* **Synchronous** - Most commonly you make a request to Tor then receive its
  reply. The :func:`~stem.control.Controller.get_info` calls in the `first
  tutorial <the_little_relay_that_could.html>`_ are an example of this.

* **Asynchronous** - Controllers can subscribe to be notified when various
  kinds of events occur within Tor (see the :data:`~stem.control.EventType`).
  Stem's users provide a callback function to
  :func:`~stem.control.Controller.add_event_listener` which is then notified
  when the event occurs.

Try to avoid lengthy operations within event callbacks. They're notified by a
single dedicated event thread, and blocking this thread will prevent the
delivery of further events.

With that out of the way lets see an example. The following is a `curses
<http://docs.python.org/2/howto/curses.html>`_ application that graphs the
bandwidth usage of Tor...

.. image:: /_static/bandwidth_graph_output.png

To do this it listens to **BW events**
(the class for which is a :class:`~stem.response.events.BandwidthEvent`). These
are events that Tor emits each second saying the number of bytes downloaded and
uploaded.

.. code-block:: python
  :emphasize-lines: 53-55,62-67

  import curses
  import functools

  from stem.control import EventType, Controller
  from stem.util import str_tools

  # colors that curses can handle

  COLOR_LIST = {
    "red": curses.COLOR_RED,
    "green": curses.COLOR_GREEN,
    "yellow": curses.COLOR_YELLOW,
    "blue": curses.COLOR_BLUE,
    "cyan": curses.COLOR_CYAN,
    "magenta": curses.COLOR_MAGENTA,
    "black": curses.COLOR_BLACK,
    "white": curses.COLOR_WHITE,
  }

  GRAPH_WIDTH = 40
  GRAPH_HEIGHT = 8

  DOWNLOAD_COLOR = "green"
  UPLOAD_COLOR = "blue"

  def main():
    with Controller.from_port(port = 9051) as controller:
      controller.authenticate()

      try:
        # This makes curses initialize and call draw_bandwidth_graph() with a
        # reference to the screen, followed by additional arguments (in this
        # case just the controller).

        curses.wrapper(draw_bandwidth_graph, controller)
      except KeyboardInterrupt:
        pass  # the user hit ctrl+c

  def draw_bandwidth_graph(stdscr, controller):
    window = Window(stdscr)

    # (downloaded, uploaded) tuples for the last 40 seconds

    bandwidth_rates = [(0, 0)] * GRAPH_WIDTH

    # Making a partial that wraps the window and bandwidth_rates with a function
    # for Tor to call when it gets a BW event. This causes the 'window' and
    # 'bandwidth_rates' to be provided as the first two arguments whenever
    # 'bw_event_handler()' is called.

    bw_event_handler = functools.partial(_handle_bandwidth_event, window, bandwidth_rates)

    # Registering this listener with Tor. Tor reports a BW event each second.

    controller.add_event_listener(bw_event_handler, EventType.BW)

    # Pause the main thread until the user hits any key... and no, don't you dare
    # ask where the 'any' key is. :P

    stdscr.getch()

  def _handle_bandwidth_event(window, bandwidth_rates, event):
    # callback for when tor provides us with a BW event

    bandwidth_rates.insert(0, (event.read, event.written))
    bandwidth_rates = bandwidth_rates[:GRAPH_WIDTH]  # truncate old values
    _render_graph(window, bandwidth_rates)

  def _render_graph(window, bandwidth_rates):
    window.erase()

    download_rates = [entry[0] for entry in bandwidth_rates]
    upload_rates = [entry[1] for entry in bandwidth_rates]

    # show the latest values at the top

    label = "Downloaded (%s/s):" % str_tools.get_size_label(download_rates[0], 1)
    window.addstr(0, 1, label, DOWNLOAD_COLOR, curses.A_BOLD)

    label = "Uploaded (%s/s):" % str_tools.get_size_label(upload_rates[0], 1)
    window.addstr(0, GRAPH_WIDTH + 7, label, UPLOAD_COLOR, curses.A_BOLD)

    # draw the graph bounds in KB

    max_download_rate = max(download_rates)
    max_upload_rate = max(upload_rates)

    window.addstr(1, 1, "%4i" % (max_download_rate / 1024), DOWNLOAD_COLOR)
    window.addstr(GRAPH_HEIGHT, 1, "   0", DOWNLOAD_COLOR)

    window.addstr(1, GRAPH_WIDTH + 7, "%4i" % (max_upload_rate / 1024), UPLOAD_COLOR)
    window.addstr(GRAPH_HEIGHT, GRAPH_WIDTH + 7, "   0", UPLOAD_COLOR)

    # draw the graph

    for col in xrange(GRAPH_WIDTH):
      col_height = GRAPH_HEIGHT * download_rates[col] / max(max_download_rate, 1)

      for row in xrange(col_height):
        window.addstr(GRAPH_HEIGHT - row, col + 6, " ", DOWNLOAD_COLOR, curses.A_STANDOUT)

      col_height = GRAPH_HEIGHT * upload_rates[col] / max(max_upload_rate, 1)

      for row in xrange(col_height):
        window.addstr(GRAPH_HEIGHT - row, col + GRAPH_WIDTH + 12, " ", UPLOAD_COLOR, curses.A_STANDOUT)

    window.refresh()

  class Window(object):
    """
    Simple wrapper for the curses standard screen object.
    """

    def __init__(self, stdscr):
      self._stdscr = stdscr

      # Mappings of names to the curses color attribute. Initially these all
      # reference black text, but if the terminal can handle color then
      # they're set with that foreground color.

      self._colors = dict([(color, 0) for color in COLOR_LIST])

      # allows for background transparency

      try:
        curses.use_default_colors()
      except curses.error:
        pass

      # makes the cursor invisible

      try:
        curses.curs_set(0)
      except curses.error:
        pass

      # initializes colors if the terminal can handle them

      try:
        if curses.has_colors():
          color_pair = 1

          for name, foreground in COLOR_LIST.items():
            background = -1  # allows for default (possibly transparent) background
            curses.init_pair(color_pair, foreground, background)
            self._colors[name] = curses.color_pair(color_pair)
            color_pair += 1
      except curses.error:
        pass

    def addstr(self, y, x, msg, color = None, attr = curses.A_NORMAL):
      # Curses throws an error if we try to draw a message that spans out of the
      # window's bounds (... seriously?), so doing our best to avoid that.

      if color is not None:
        if color not in self._colors:
          recognized_colors = ", ".join(self._colors.keys())
          raise ValueError("The '%s' color isn't recognized: %s" % (color, recognized_colors))

        attr |= self._colors[color]

      max_y, max_x = self._stdscr.getmaxyx()

      if max_x > x and max_y > y:
        try:
          self._stdscr.addstr(y, x, msg[:max_x - x], attr)
        except:
          pass  # maybe an edge case while resizing the window

    def erase(self):
      self._stdscr.erase()

    def refresh(self):
      self._stdscr.refresh()

  if __name__ == '__main__':
    main()