Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions doc/examples/ami.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,36 +86,52 @@ for information::
self._manager.monitor_connection()

def _register_callbacks(self):
#This sets up some event callbacks, so that interesting things, like calls being
#established or torn down, will be processed by your application's logic. Of course,
#since this is just an example, the same event will be registered using two different
#methods.
#This sets up some event callbacks so that interesting things, like calls being
#established or torn down, are processed by the application's logic.

#The event that will be registered is 'FullyBooted', sent by Asterisk immediately after
#connecting, to indicate that everything is online. What the following code does is
#register two different callback-handlers for this event using two different
#match-methods: string comparison and class-match. String-matching and class-resolution
#are equal in performance, so choose whichever you think looks better.
#pystrix supports four callback patterns:
#
# 1. Exact string match — fires when the wire event name matches exactly.
# 2. Class match — fires when the event has been typed to the given class.
# This works for all built-in classes and any class registered with
# pystrix.ami.register_event_class() (see below).
# 3. Empty string '' — a catch-all that fires for every event, useful for
# logging or debugging.
# 4. None — fires for responses not associated with any request, which
# typically indicates a glitch or a timed-out request.
#
#For events pystrix does not recognise, the event still arrives as a
#generic _Event object with all headers intact. Register against its wire
#name as a plain string to receive it.

#Register 'FullyBooted' two ways to illustrate that string and class
#matching are equivalent in performance and semantics.
self._manager.register_callback('FullyBooted', self._handle_string_event)
self._manager.register_callback(pystrix.ami.core_events.FullyBooted, self._handle_class_event)
#Now, when 'FullyBooted' is received, both handlers will be invoked in the order in
#which they were registered.
#Both handlers are invoked in registration order when 'FullyBooted' arrives.

#A catch-all-handler can be set using the empty string as a qualifier, causing it to
#receive every event emitted by Asterisk, which may be useful for debugging purposes.
#Catch every event, regardless of type.
self._manager.register_callback('', self._handle_event)

#Additionally, an orphan-handler may be provided using the special qualifier None,
#causing any responses not associated with a request to be received. This should only
#apply to glitches in pre-production versions of Asterisk or requests that timed out
#while waiting for a response, which is also indicative of glitchy behaviour. This
#handler could be used to process the orphaned response in special cases, but is likely
#best relegated to a logging role.
#Catch orphaned responses (those with no matching request).
self._manager.register_callback(None, self._handle_event)

#And here's another example of a registered event, this time catching Asterisk's
#Shutdown signal, emitted when the system is shutting down.
#Catch Asterisk's Shutdown signal using a plain string.
self._manager.register_callback('Shutdown', self._handle_shutdown)

#To use a custom typed class for an event that pystrix does not define
#natively, register it before calling connect(). After registration,
#both type-mutation on arrival and class-based callbacks work normally.
#
# class MyQueueCallerJoin(pystrix.ami.ami._Event):
# pass
#
# pystrix.ami.register_event_class(MyQueueCallerJoin)
# self._manager.register_callback(MyQueueCallerJoin, self._handle_queue_event)
#
#Supply name= when the wire event name differs from the class name:
#
# pystrix.ami.register_event_class(MyClass, name='QueueCallerJoin')

def _handle_shutdown(self, event, manager):
self._kill_flag = True
Expand Down
19 changes: 15 additions & 4 deletions pystrix/ami/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
Manager,
ManagerError,
ManagerSocketError,
_Aggregate,
_Event,
register_event_class,
unregister_event_class,
)

for module in (
Expand All @@ -67,9 +71,16 @@
app_confbridge_events,
app_meetme_events,
):
for event in (e for e in dir(module) if not e.startswith("_")):
class_object = getattr(module, event)
_EVENT_REGISTRY[event] = class_object
_EVENT_REGISTRY_REV[class_object] = event
for name in (e for e in dir(module) if not e.startswith("_")):
class_object = getattr(module, name)
if not (
isinstance(class_object, type)
and issubclass(class_object, (_Event, _Aggregate))
):
continue
_EVENT_REGISTRY[name] = class_object
_EVENT_REGISTRY_REV[class_object] = name
del _EVENT_REGISTRY
del _EVENT_REGISTRY_REV
del _Event
del _Aggregate
60 changes: 60 additions & 0 deletions pystrix/ami/ami.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,66 @@
_EVENT_REGISTRY = {} # Meant to be internally managed only, this provides mappings from event-class-names to the classes, to enable type-mutation
_EVENT_REGISTRY_REV = {} # Provides the friendly names of events as strings, keyed by class object


def register_event_class(event_class, name=None):
"""
Registers a custom event class so that pystrix can type-mutate incoming AMI events
to it and class-based callbacks work with ``register_callback``.

``event_class`` must be a subclass of :class:`_Event`; a :exc:`ValueError` is raised
otherwise. ``name`` defaults to ``event_class.__name__``, which should match the
wire event name Asterisk sends. Supply an explicit ``name`` only when the desired
wire name differs from the class name.

Register custom classes before calling :meth:`Manager.connect` to avoid a race
where events arrive before the class is registered. Registration is not
thread-safe; call this function from the main thread before connecting.

Re-registering a class under a different name after callbacks have been wired
with :meth:`Manager.register_callback` does not update those callbacks; they
remain bound to the original wire name.
"""
if not (isinstance(event_class, type) and issubclass(event_class, _Event)):
raise ValueError(
f"event_class must be a subclass of _Event, got {event_class!r}"
)
event_name = name or event_class.__name__
# Evict any class that previously held this wire name to prevent a stale reverse entry.
old_class = _EVENT_REGISTRY.get(event_name)
if old_class is not None and old_class is not event_class:
_EVENT_REGISTRY_REV.pop(old_class, None)
# Evict any wire name this class was previously registered under to prevent a zombie
# forward entry when the same class is re-registered under a different name.
old_name = _EVENT_REGISTRY_REV.get(event_class)
if old_name is not None and old_name != event_name:
_EVENT_REGISTRY.pop(old_name, None)
_EVENT_REGISTRY[event_name] = event_class
_EVENT_REGISTRY_REV[event_class] = event_name


def unregister_event_class(event_class_or_name):
"""
Removes the registration previously added with :func:`register_event_class`.

``event_class_or_name`` may be the class object or the wire name string that was
passed as the ``name=`` argument to :func:`register_event_class` (which may differ
from ``event_class.__name__`` when an explicit name was supplied).
Returns ``True`` if a registration was removed, ``False`` if nothing matched.
"""
if isinstance(event_class_or_name, str):
event_class = _EVENT_REGISTRY.pop(event_class_or_name, None)
if event_class is None:
return False
_EVENT_REGISTRY_REV.pop(event_class, None)
return True
else:
event_name = _EVENT_REGISTRY_REV.pop(event_class_or_name, None)
if event_name is None:
return False
_EVENT_REGISTRY.pop(event_name, None)
return True


_EOC = "--END COMMAND--" # A string used by Asterisk to mark the end of some of its responses.
_EOL = "\r\n" # Asterisk uses CRLF linebreaks to mark the ends of its lines.
_EOL_FAKE = (
Expand Down
164 changes: 164 additions & 0 deletions pystrix/ami/core_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,170 @@ class VoicemailUserEntryComplete(_Event):
"""


# Modern channel and bridge events
####################################################################################################
# These cover events added in Asterisk 13+ that are commonly needed in call-control applications.
# Wire names confirmed against Asterisk 13 main/manager_channels.c and main/manager_bridges.c.


class BridgeCreate(_Event):
"""
Emitted when a bridge is created.

- 'BridgeType': The type of bridge
- 'BridgeUniqueid': A unique identifier for the bridge
- 'BridgeTechnology': The technology used by the bridge
- 'BridgeNumChannels': The number of channels currently in the bridge (int)
"""

def process(self):
"""
Translates the 'BridgeNumChannels' header's value into an int, or None on failure.
"""
(headers, data) = _Event.process(self)
generic_transforms.to_int(headers, ("BridgeNumChannels",), None)
return (headers, data)


class BridgeDestroy(_Event):
"""
Emitted when a bridge is destroyed.

- 'BridgeType': The type of bridge
- 'BridgeUniqueid': A unique identifier for the bridge
- 'BridgeTechnology': The technology used by the bridge
- 'BridgeNumChannels': The number of channels at time of destruction (int)
"""

def process(self):
"""
Translates the 'BridgeNumChannels' header's value into an int, or None on failure.
"""
(headers, data) = _Event.process(self)
generic_transforms.to_int(headers, ("BridgeNumChannels",), None)
return (headers, data)


class BridgeEnter(_Event):
"""
Emitted when a channel enters a bridge.

- 'BridgeType': The type of bridge
- 'BridgeUniqueid': A unique identifier for the bridge
- 'BridgeTechnology': The technology used by the bridge
- 'BridgeNumChannels': The number of channels now in the bridge (int)
- 'Channel': The channel that entered the bridge
- 'Uniqueid': The Asterisk unique identifier for the channel
- 'SwapUniqueid': The unique identifier of a channel swapped out (empty when no swap)
"""

def process(self):
"""
Translates the 'BridgeNumChannels' header's value into an int, or None on failure.
"""
(headers, data) = _Event.process(self)
generic_transforms.to_int(headers, ("BridgeNumChannels",), None)
return (headers, data)


class BridgeLeave(_Event):
"""
Emitted when a channel leaves a bridge.

- 'BridgeType': The type of bridge
- 'BridgeUniqueid': A unique identifier for the bridge
- 'BridgeTechnology': The technology used by the bridge
- 'BridgeNumChannels': The number of channels remaining in the bridge (int)
- 'Channel': The channel that left the bridge
- 'Uniqueid': The Asterisk unique identifier for the channel
"""

def process(self):
"""
Translates the 'BridgeNumChannels' header's value into an int, or None on failure.
"""
(headers, data) = _Event.process(self)
generic_transforms.to_int(headers, ("BridgeNumChannels",), None)
return (headers, data)


class DialBegin(_Event):
"""
Emitted when a dial operation begins on a channel.

- 'Channel': The calling channel
- 'Uniqueid': The Asterisk unique identifier for the calling channel
- 'DestChannel': The channel being dialled
- 'DestUniqueid': The Asterisk unique identifier for the destination channel
- 'DialString': The dial string used to reach the destination
"""


class DialEnd(_Event):
"""
Emitted when a dial operation ends.

- 'Channel': The calling channel
- 'Uniqueid': The Asterisk unique identifier for the calling channel
- 'DestChannel': The channel that was dialled
- 'DestUniqueid': The Asterisk unique identifier for the destination channel
- 'DialStatus': The outcome: "ANSWER", "BUSY", "CANCEL", "CONGESTION", "NOANSWER", "CHANUNAVAIL"
- 'Forward' (optional): The forwarding destination, if the call was forwarded
"""


class Hold(_Event):
"""
Emitted when a channel is placed on hold.

- 'MusicClass' (optional): The suggested music-on-hold class
"""


class MusicOnHoldStart(_Event):
"""
Emitted when music on hold starts on a channel.

- 'Class': The music-on-hold class being played
"""


class MusicOnHoldStop(_Event):
"""
Emitted when music on hold stops on a channel.
"""


class NewCallerid(_Event):
"""
Emitted when a channel receives new Caller ID information.

- 'Channel': The channel whose Caller ID changed
- 'Uniqueid': The Asterisk unique identifier for the channel
- 'CallerIDNum': The new caller ID number
- 'CallerIDName': The new caller ID name
- 'CID-CallingPres': A description of the Caller ID presentation, e.g.
``'0 (Presentation Allowed, Not Screened)'``; always a string, not an int
"""


class NewConnectedLine(_Event):
"""
Emitted when a channel's connected line information changes.

- 'Channel': The channel whose connected line changed
- 'Uniqueid': The Asterisk unique identifier for the channel
- 'ConnectedLineNum': The connected line number (may be alphanumeric)
- 'ConnectedLineName': The connected line name
"""


class Unhold(_Event):
"""
Emitted when a channel is taken off hold.
"""


# List-aggregation events
####################################################################################################
# These define non-Asterisk-native event-types that collect multiple events (cases where multiple
Expand Down
Loading