From 87194dfa8e828a61e2b556de464c0759498f5312 Mon Sep 17 00:00:00 2001 From: Samuel Fortier-Galarneau Date: Fri, 29 Aug 2014 14:37:33 -0500 Subject: [PATCH] client/model: Allow for variable arguments to be passed to callbacks This patch allows a user of ari-py to pass arguments (*args, **kwargs) to callback functions registered for events. This is particularly useful when maintaining state; the current state can be passed around the callbacks as opposed to being maintained globally. Note that passing said arguments is completely optional. --- ari/client.py | 93 ++++++++++++++++++++++++++++---------- ari/model.py | 14 +++--- ari_test/websocket_test.py | 33 ++++++++++++++ setup.py | 2 +- 4 files changed, 110 insertions(+), 32 deletions(-) diff --git a/ari/client.py b/ari/client.py index dabbf79..c1ce6e7 100644 --- a/ari/client.py +++ b/ari/client.py @@ -94,7 +94,10 @@ def __run(self, ws): for listener in listeners: # noinspection PyBroadException try: - listener(msg_json) + callback, args, kwargs = listener + args = args or () + kwargs = kwargs or {} + callback(msg_json, *args, **kwargs) except Exception as e: self.exception_handler(e) @@ -117,15 +120,21 @@ def run(self, apps): ws.close() self.websockets.remove(ws) - def on_event(self, event_type, event_cb): + def on_event(self, event_type, event_cb, *args, **kwargs): """Register callback for events with given type. :param event_type: String name of the event to register for. :param event_cb: Callback function :type event_cb: (dict) -> None + :param args: Arguments to pass to event_cb + :param kwargs: Keyword arguments to pass to event_cb """ - listeners = self.event_listeners.setdefault(event_type, set()) - listeners.add(event_cb) + listeners = self.event_listeners.setdefault(event_type, list()) + for cb in listeners: + if event_cb == cb[0]: + listeners.remove(cb) + callback_obj = (event_cb, args, kwargs) + listeners.append(callback_obj) client = self class EventUnsubscriber(object): @@ -135,11 +144,13 @@ class EventUnsubscriber(object): def close(self): """Unsubscribe the associated event callback. """ - client.event_listeners[event_type].discard(event_cb) + if callback_obj in client.event_listeners[event_type]: + client.event_listeners[event_type].remove(callback_obj) return EventUnsubscriber() - def on_object_event(self, event_type, event_cb, factory_fn, model_id): + def on_object_event(self, event_type, event_cb, factory_fn, model_id, + *args, **kwargs): """Register callback for events with the given type. Event fields of the given model_id type are passed along to event_cb. @@ -151,6 +162,8 @@ def on_object_event(self, event_type, event_cb, factory_fn, model_id): :type event_cb: (Obj, dict) -> None or (dict[str, Obj], dict) -> :param factory_fn: Function for creating Obj from JSON :param model_id: String id for Obj from Swagger models. + :param args: Arguments to pass to event_cb + :param kwargs: Keyword arguments to pass to event_cb """ # Find the associated model from the Swagger declaration event_model = self.event_models.get(event_type) @@ -164,10 +177,13 @@ def on_object_event(self, event_type, event_cb, factory_fn, model_id): raise ValueError("Event model '%s' has no fields of type %s" % (event_type, model_id)) - def extract_objects(event): + def extract_objects(event, *args, **kwargs): """Extract objects of a given type from an event. :param event: Event + :param args: Arguments to pass to the event callback + :param kwargs: Keyword arguments to pass to the event + callback """ # Extract the fields which are of the expected type obj = {obj_field: factory_fn(self, event[obj_field]) @@ -179,78 +195,105 @@ def extract_objects(event): obj = obj.values()[0] else: obj = None - event_cb(obj, event) + event_cb(obj, event, *args, **kwargs) - return self.on_event(event_type, extract_objects) + return self.on_event(event_type, extract_objects, + *args, + **kwargs) - def on_channel_event(self, event_type, fn): + def on_channel_event(self, event_type, fn, *args, **kwargs): """Register callback for Channel related events :param event_type: String name of the event to register for. :param fn: Callback function :type fn: (Channel, dict) -> None or (list[Channel], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, Channel, 'Channel') + return self.on_object_event(event_type, fn, Channel, 'Channel', + *args, **kwargs) - def on_bridge_event(self, event_type, fn): + def on_bridge_event(self, event_type, fn, *args, **kwargs): """Register callback for Bridge related events :param event_type: String name of the event to register for. :param fn: Callback function :type fn: (Bridge, dict) -> None or (list[Bridge], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, Bridge, 'Bridge') + return self.on_object_event(event_type, fn, Bridge, 'Bridge', + *args, **kwargs) - def on_playback_event(self, event_type, fn): + def on_playback_event(self, event_type, fn, *args, **kwargs): """Register callback for Playback related events :param event_type: String name of the event to register for. :param fn: Callback function :type fn: (Playback, dict) -> None or (list[Playback], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, Playback, 'Playback') + return self.on_object_event(event_type, fn, Playback, 'Playback', + *args, **kwargs) - def on_live_recording_event(self, event_type, fn): + def on_live_recording_event(self, event_type, fn, *args, **kwargs): """Register callback for LiveRecording related events :param event_type: String name of the event to register for. :param fn: Callback function :type fn: (LiveRecording, dict) -> None or (list[LiveRecording], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, LiveRecording, 'LiveRecording') + return self.on_object_event(event_type, fn, LiveRecording, + 'LiveRecording', *args, **kwargs) - def on_stored_recording_event(self, event_type, fn): + def on_stored_recording_event(self, event_type, fn, *args, **kwargs): """Register callback for StoredRecording related events :param event_type: String name of the event to register for. :param fn: Callback function :type fn: (StoredRecording, dict) -> None or (list[StoredRecording], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, StoredRecording, 'StoredRecording') + return self.on_object_event(event_type, fn, StoredRecording, + 'StoredRecording', *args, **kwargs) - def on_endpoint_event(self, event_type, fn): + def on_endpoint_event(self, event_type, fn, *args, **kwargs): """Register callback for Endpoint related events :param event_type: String name of the event to register for. :param fn: Callback function :type fn: (Endpoint, dict) -> None or (list[Endpoint], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, Endpoint, 'Endpoint') + return self.on_object_event(event_type, fn, Endpoint, 'Endpoint', + *args, **kwargs) - def on_device_state_event(self, event_type, fn): + def on_device_state_event(self, event_type, fn, *args, **kwargs): """Register callback for DeviceState related events :param event_type: String name of the event to register for. :param fn: Callback function :type fn: (DeviceState, dict) -> None or (list[DeviceState], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, DeviceState, 'DeviceState') + return self.on_object_event(event_type, fn, DeviceState, 'DeviceState', + *args, **kwargs) - def on_sound_event(self, event_type, fn): + def on_sound_event(self, event_type, fn, *args, **kwargs): """Register callback for Sound related events :param event_type: String name of the event to register for. :param fn: Sound function :type fn: (Sound, dict) -> None or (list[Sound], dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - return self.on_object_event(event_type, fn, Sound, 'Sound') + return self.on_object_event(event_type, fn, Sound, 'Sound', + *args, **kwargs) + diff --git a/ari/model.py b/ari/model.py index ef0f8c7..0a6aad1 100644 --- a/ari/model.py +++ b/ari/model.py @@ -156,33 +156,35 @@ def enrich_operation(**kwargs): return enrich_operation - def on_event(self, event_type, fn): + def on_event(self, event_type, fn, *args, **kwargs): """Register event callbacks for this specific domain object. :param event_type: Type of event to register for. :type event_type: str :param fn: Callback function for events. :type fn: (object, dict) -> None + :param args: Arguments to pass to fn + :param kwargs: Keyword arguments to pass to fn """ - def fn_filter(objects, event): - """Filter recieved events for this object. + def fn_filter(objects, event, *args, **kwargs): + """Filter received events for this object. :param objects: Objects found in this event. :param event: Event. """ if isinstance(objects, dict): if self.id in [c.id for c in objects.values()]: - fn(objects, event) + fn(objects, event, *args, **kwargs) else: if self.id == objects.id: - fn(objects, event) + fn(objects, event, *args, **kwargs) if not self.event_reg: msg = "Event callback registration called on object with no events" raise RuntimeError(msg) - return self.event_reg(event_type, fn_filter) + return self.event_reg(event_type, fn_filter, *args, **kwargs) class Channel(BaseObject): diff --git a/ari_test/websocket_test.py b/ari_test/websocket_test.py index 25fe564..c99ee00 100644 --- a/ari_test/websocket_test.py +++ b/ari_test/websocket_test.py @@ -140,6 +140,39 @@ def cb(channel, event): ] self.assertEqual(expected, self.actual) + def test_arbitrary_callback_arguments(self): + self.serve(GET, 'channels', 'test-channel', + body='{"id": "test-channel"}') + self.serve(DELETE, 'channels', 'test-channel') + messages = [ + '{"type": "ChannelDtmfReceived", "channel": {"id": "test-channel"}}' + ] + obj = {'key': 'val'} + + uut = connect(BASE_URL, messages) + channel = uut.channels.get(channelId='test-channel') + + def cb(channel, event, arg): + if arg == 'done': + channel.hangup() + else: + self.record_event(arg) + + def cb2(channel, event, arg1, arg2=None, arg3=None): + self.record_event(arg1) + self.record_event(arg2) + self.record_event(arg3) + + channel.on_event('ChannelDtmfReceived', cb, 1) + channel.on_event('ChannelDtmfReceived', cb, arg=2) + channel.on_event('ChannelDtmfReceived', cb, obj) + channel.on_event('ChannelDtmfReceived', cb2, 2.0, arg3=[1, 2, 3]) + channel.on_event('ChannelDtmfReceived', cb, 'done') + uut.run('test') + + expected = [1, 2, obj, 2.0, None, [1, 2, 3]] + self.assertEqual(expected, self.actual) + def test_bad_event_type(self): uut = connect(BASE_URL, []) try: diff --git a/setup.py b/setup.py index 00a6a8c..22d68ce 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="ari", - version="0.1.2", + version="0.1.3", license="BSD 3-Clause License", description="Library for accessing the Asterisk REST Interface", long_description=open(os.path.join(os.path.dirname(__file__),