Skip to content

Commit

Permalink
client/model: Allow for variable arguments to be passed to callbacks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Samuel Fortier-Galarneau committed Aug 29, 2014
1 parent 75f8138 commit 87194df
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 32 deletions.
93 changes: 68 additions & 25 deletions ari/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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])
Expand All @@ -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)

14 changes: 8 additions & 6 deletions ari/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
33 changes: 33 additions & 0 deletions ari_test/websocket_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__),
Expand Down

0 comments on commit 87194df

Please sign in to comment.