diff options
-rw-r--r-- | docs/index.rst | 14 | ||||
-rw-r--r-- | mastodon/streaming.py | 50 | ||||
-rw-r--r-- | tests/test_streaming.py | 40 |
3 files changed, 89 insertions, 15 deletions
diff --git a/docs/index.rst b/docs/index.rst index 473777a..e7b1ed9 100644 --- a/docs/index.rst +++ b/docs/index.rst | |||
@@ -1275,6 +1275,19 @@ The streaming functions take instances of `StreamListener` as the `listener` par | |||
1275 | A `CallbackStreamListener` class that allows you to specify function callbacks | 1275 | A `CallbackStreamListener` class that allows you to specify function callbacks |
1276 | directly is included for convenience. | 1276 | directly is included for convenience. |
1277 | 1277 | ||
1278 | For new well-known events implement the streaming function in `StreamListener` or `CallbackStreamListener`. | ||
1279 | The function name is `on_` + the event name. If the event-name contains dots, use an underscore instead. | ||
1280 | |||
1281 | E.g. for `'status.update'` the listener function should be named as `on_status_update`. | ||
1282 | |||
1283 | It may be that future Mastodon versions will come with completely new (unknown) event names. In this | ||
1284 | case a (deprecated) Mastodon.py would throw an error. If you want to avoid this in general, you can | ||
1285 | override the listener function `on_unknown_event`. This has an additional parameter `name` which informs | ||
1286 | about the name of the event. `unknown_event` contains the content of the event. | ||
1287 | |||
1288 | Alternatively, a callback function can be passed in the `unknown_event_handler` parameter in the | ||
1289 | `CallbackStreamListener` constructor. | ||
1290 | |||
1278 | When in not-async mode or async mode without async_reconnect, the stream functions may raise | 1291 | When in not-async mode or async mode without async_reconnect, the stream functions may raise |
1279 | various exceptions: `MastodonMalformedEventError` if a received event cannot be parsed and | 1292 | various exceptions: `MastodonMalformedEventError` if a received event cannot be parsed and |
1280 | `MastodonNetworkError` if any connection problems occur. | 1293 | `MastodonNetworkError` if any connection problems occur. |
@@ -1294,6 +1307,7 @@ StreamListener | |||
1294 | .. automethod:: StreamListener.on_notification | 1307 | .. automethod:: StreamListener.on_notification |
1295 | .. automethod:: StreamListener.on_delete | 1308 | .. automethod:: StreamListener.on_delete |
1296 | .. automethod:: StreamListener.on_conversation | 1309 | .. automethod:: StreamListener.on_conversation |
1310 | .. automethod:: StreamListener.on_unknown_event | ||
1297 | .. automethod:: StreamListener.on_abort | 1311 | .. automethod:: StreamListener.on_abort |
1298 | .. automethod:: StreamListener.handle_heartbeat | 1312 | .. automethod:: StreamListener.handle_heartbeat |
1299 | 1313 | ||
diff --git a/mastodon/streaming.py b/mastodon/streaming.py index 214ed1c..ceb61ea 100644 --- a/mastodon/streaming.py +++ b/mastodon/streaming.py | |||
@@ -45,6 +45,16 @@ class StreamListener(object): | |||
45 | contains the resulting conversation dict.""" | 45 | contains the resulting conversation dict.""" |
46 | pass | 46 | pass |
47 | 47 | ||
48 | def on_unknown_event(self, name, unknown_event = None): | ||
49 | """An unknown mastodon API event has been received. The name contains the event-name and unknown_event | ||
50 | contains the content of the unknown event. | ||
51 | |||
52 | This function must be implemented, if unknown events should be handled without an error. | ||
53 | """ | ||
54 | exception = MastodonMalformedEventError('Bad event type', name) | ||
55 | self.on_abort(exception) | ||
56 | raise exception | ||
57 | |||
48 | def handle_heartbeat(self): | 58 | def handle_heartbeat(self): |
49 | """The server has sent us a keep-alive message. This callback may be | 59 | """The server has sent us a keep-alive message. This callback may be |
50 | useful to carry out periodic housekeeping tasks, or just to confirm | 60 | useful to carry out periodic housekeeping tasks, or just to confirm |
@@ -56,6 +66,11 @@ class StreamListener(object): | |||
56 | Handles a stream of events from the Mastodon server. When each event | 66 | Handles a stream of events from the Mastodon server. When each event |
57 | is received, the corresponding .on_[name]() method is called. | 67 | is received, the corresponding .on_[name]() method is called. |
58 | 68 | ||
69 | When the Mastodon API changes, the on_unknown_event(name, content) | ||
70 | function is called. | ||
71 | The default behavior is to throw an error. Define a callback handler | ||
72 | to intercept unknown events if needed (and avoid errors) | ||
73 | |||
59 | response; a requests response object with the open stream for reading. | 74 | response; a requests response object with the open stream for reading. |
60 | """ | 75 | """ |
61 | event = {} | 76 | event = {} |
@@ -137,33 +152,32 @@ class StreamListener(object): | |||
137 | exception, | 152 | exception, |
138 | err | 153 | err |
139 | ) | 154 | ) |
140 | 155 | # New mastodon API also supports event names with dots: | |
141 | handler_name = 'on_' + name | 156 | handler_name = 'on_' + name.replace('.', '_') |
142 | try: | 157 | # A generic way to handle unknown events to make legacy code more stable for future changes |
143 | handler = getattr(self, handler_name) | 158 | handler = getattr(self, handler_name, self.on_unknown_event) |
144 | except AttributeError as err: | 159 | if handler != self.on_unknown_event: |
145 | exception = MastodonMalformedEventError('Bad event type', name) | ||
146 | self.on_abort(exception) | ||
147 | six.raise_from( | ||
148 | exception, | ||
149 | err | ||
150 | ) | ||
151 | else: | ||
152 | handler(payload) | 160 | handler(payload) |
161 | else: | ||
162 | handler(name, payload) | ||
163 | |||
153 | 164 | ||
154 | class CallbackStreamListener(StreamListener): | 165 | class CallbackStreamListener(StreamListener): |
155 | """ | 166 | """ |
156 | Simple callback stream handler class. | 167 | Simple callback stream handler class. |
157 | Can optionally additionally send local update events to a separate handler. | 168 | Can optionally additionally send local update events to a separate handler. |
169 | Define an unknown_event_handler for new Mastodon API events. If not, the | ||
170 | listener will raise an error on new, not handled, events from the API. | ||
158 | """ | 171 | """ |
159 | def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None, conversation_handler = None): | 172 | def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None, conversation_handler = None, unknown_event_handler = None): |
160 | super(CallbackStreamListener, self).__init__() | 173 | super(CallbackStreamListener, self).__init__() |
161 | self.update_handler = update_handler | 174 | self.update_handler = update_handler |
162 | self.local_update_handler = local_update_handler | 175 | self.local_update_handler = local_update_handler |
163 | self.delete_handler = delete_handler | 176 | self.delete_handler = delete_handler |
164 | self.notification_handler = notification_handler | 177 | self.notification_handler = notification_handler |
165 | self.conversation_handler = conversation_handler | 178 | self.conversation_handler = conversation_handler |
166 | 179 | self.unknown_event_handler = unknown_event_handler | |
180 | |||
167 | def on_update(self, status): | 181 | def on_update(self, status): |
168 | if self.update_handler != None: | 182 | if self.update_handler != None: |
169 | self.update_handler(status) | 183 | self.update_handler(status) |
@@ -188,3 +202,11 @@ class CallbackStreamListener(StreamListener): | |||
188 | def on_conversation(self, conversation): | 202 | def on_conversation(self, conversation): |
189 | if self.conversation_handler != None: | 203 | if self.conversation_handler != None: |
190 | self.conversation_handler(conversation) | 204 | self.conversation_handler(conversation) |
205 | |||
206 | def on_unknown_event(self, name, unknown_event = None): | ||
207 | if self.unknown_event_handler != None: | ||
208 | self.unknown_event_handler(name, unknown_event) | ||
209 | else: | ||
210 | exception = MastodonMalformedEventError('Bad event type', name) | ||
211 | self.on_abort(exception) | ||
212 | raise exception | ||
diff --git a/tests/test_streaming.py b/tests/test_streaming.py index cddb79a..8912b9c 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py | |||
@@ -61,6 +61,8 @@ class Listener(StreamListener): | |||
61 | self.notifications = [] | 61 | self.notifications = [] |
62 | self.deletes = [] | 62 | self.deletes = [] |
63 | self.heartbeats = 0 | 63 | self.heartbeats = 0 |
64 | self.bla_called = False | ||
65 | self.do_something_called = False | ||
64 | 66 | ||
65 | def on_update(self, status): | 67 | def on_update(self, status): |
66 | self.updates.append(status) | 68 | self.updates.append(status) |
@@ -72,6 +74,11 @@ class Listener(StreamListener): | |||
72 | self.deletes.append(status_id) | 74 | self.deletes.append(status_id) |
73 | 75 | ||
74 | def on_blahblah(self, data): | 76 | def on_blahblah(self, data): |
77 | self.bla_called = True | ||
78 | pass | ||
79 | |||
80 | def on_do_something(self, data): | ||
81 | self.do_something_called = True | ||
75 | pass | 82 | pass |
76 | 83 | ||
77 | def handle_heartbeat(self): | 84 | def handle_heartbeat(self): |
@@ -158,6 +165,37 @@ def test_unknown_event(): | |||
158 | 'data: {}', | 165 | 'data: {}', |
159 | '', | 166 | '', |
160 | ]) | 167 | ]) |
168 | assert listener.bla_called == True | ||
169 | assert listener.updates == [] | ||
170 | assert listener.notifications == [] | ||
171 | assert listener.deletes == [] | ||
172 | assert listener.heartbeats == 0 | ||
173 | |||
174 | def test_unknown_handled_event(): | ||
175 | """Be tolerant of new unknown event types, if on_unknown_event is available""" | ||
176 | listener = Listener() | ||
177 | listener.on_unknown_event = lambda name, payload: None | ||
178 | |||
179 | listener.handle_stream_([ | ||
180 | 'event: complete.new.event', | ||
181 | 'data: {"k": "v"}', | ||
182 | '', | ||
183 | ]) | ||
184 | |||
185 | assert listener.updates == [] | ||
186 | assert listener.notifications == [] | ||
187 | assert listener.deletes == [] | ||
188 | assert listener.heartbeats == 0 | ||
189 | |||
190 | def test_dotted_unknown_event(): | ||
191 | """Be tolerant of new event types with dots in the event-name""" | ||
192 | listener = Listener() | ||
193 | listener.handle_stream_([ | ||
194 | 'event: do.something', | ||
195 | 'data: {}', | ||
196 | '', | ||
197 | ]) | ||
198 | assert listener.do_something_called == True | ||
161 | assert listener.updates == [] | 199 | assert listener.updates == [] |
162 | assert listener.notifications == [] | 200 | assert listener.notifications == [] |
163 | assert listener.deletes == [] | 201 | assert listener.deletes == [] |
@@ -169,7 +207,7 @@ def test_invalid_event(): | |||
169 | with pytest.raises(MastodonMalformedEventError): | 207 | with pytest.raises(MastodonMalformedEventError): |
170 | listener.handle_stream_([ | 208 | listener.handle_stream_([ |
171 | 'event: whatup', | 209 | 'event: whatup', |
172 | 'data: {}', | 210 | 'data: {"k": "v"}', |
173 | '', | 211 | '', |
174 | ]) | 212 | ]) |
175 | 213 | ||