- copy original 0.2 release from tags/0.2
git-svn-id: https://pyst.svn.sourceforge.net/svnroot/pyst/pyst/trunk@2 01a3061f-1c3a-49da-a2a0-fa5697faa6a0develop
parent
dd33d0c724
commit
0cfa2e82ef
|
@ -0,0 +1,78 @@
|
||||||
|
2006-11-28 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* UPGRADE: Tweaked formatting.
|
||||||
|
|
||||||
|
2006-10-30 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* ChangeLog: Fixed previous entry.
|
||||||
|
|
||||||
|
2006-10-30 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* TODO: Updated.
|
||||||
|
* asterisk/agi.py (AGI.control_stream_file): Changed default skipms
|
||||||
|
and quoted arguments.
|
||||||
|
|
||||||
|
2006-10-24 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* asterisk/agi.py: Added get_variable_full command.
|
||||||
|
|
||||||
|
2006-10-18 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* asterisk/agitb.py: Make error output default to sys.stderr instead
|
||||||
|
of sys.stdout.
|
||||||
|
|
||||||
|
2006-09-19 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* debian/control: Removed XS-Python-Versions header to make it default
|
||||||
|
to all python versions.
|
||||||
|
|
||||||
|
2006-09-19 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* setup.py: Updated version.
|
||||||
|
|
||||||
|
2006-09-19 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* debian/rules: Changed to use pysupport.
|
||||||
|
* debian/control: Changed to use pysupport and changed arch to all.
|
||||||
|
|
||||||
|
2006-09-19 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* MANIFEST.in: Added NEWS to manifest.
|
||||||
|
|
||||||
|
2006-09-19 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* debian/rules: Updated to reflect new python policy.
|
||||||
|
* debian/control: Updated to reflect new python policy.
|
||||||
|
* debian/changelog: Updated.
|
||||||
|
|
||||||
|
2006-08-23 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* UPGRADE: Updated.
|
||||||
|
|
||||||
|
2006-08-23 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* asterisk/manager.py (unregister_event): Added.
|
||||||
|
|
||||||
|
2006-08-23 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* NEWS: Added.
|
||||||
|
|
||||||
|
2006-07-14 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* asterisk/agi.py (wait_for_digit): Only catch ValueError, not all
|
||||||
|
exceptions.
|
||||||
|
|
||||||
|
2006-07-14 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* TODO: Updated.
|
||||||
|
* asterisk/agi.py (set_variable): Documentation changes.
|
||||||
|
* asterisk/agi.py (get_variable): Changed to return and empty string
|
||||||
|
instead of throwing an exception when a channel variable is not set.
|
||||||
|
* UPGRADE: Added.
|
||||||
|
|
||||||
|
2006-07-14 Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
|
||||||
|
* ChangeLog: Added.
|
||||||
|
* TODO: Added.
|
||||||
|
* MANIFEST.in: Added ChangeLog and TODO.
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
include debian/watch
|
||||||
|
include debian/rules
|
||||||
|
include debian/changelog
|
||||||
|
include debian/control
|
||||||
|
include debian/compat
|
||||||
|
include debian/copyright
|
||||||
|
include rpm/python-pyst.spec
|
||||||
|
include ChangeLog
|
||||||
|
include TODO
|
||||||
|
include UPGRADE
|
||||||
|
include NEWS
|
||||||
|
include MANIFEST.in
|
|
@ -0,0 +1,10 @@
|
||||||
|
Metadata-Version: 1.0
|
||||||
|
Name: pyst
|
||||||
|
Version: 0.2
|
||||||
|
Summary: Asterisk related utility modules.
|
||||||
|
Home-page: http://www.sourceforge.net/projects/pyst/
|
||||||
|
Author: Karl Putland
|
||||||
|
Author-email: kputland@users.sourceforge.net
|
||||||
|
License: Python Software Foundation License (agitb), Lesser General Public License
|
||||||
|
Description: UNKNOWN
|
||||||
|
Platform: UNIX
|
|
@ -0,0 +1,22 @@
|
||||||
|
= Things to do for pyst =
|
||||||
|
|
||||||
|
= ChangeLog =
|
||||||
|
|
||||||
|
The ChangeLog needs to be updated from the monotone logs.
|
||||||
|
|
||||||
|
= Documentation =
|
||||||
|
|
||||||
|
All of pyst's inline documentation needs to be updated.
|
||||||
|
|
||||||
|
= manager.py =
|
||||||
|
|
||||||
|
This should be convereted to be single threaded. Also there is a race
|
||||||
|
condition when a user calls manager.logoff() followed by manager.close(). The
|
||||||
|
close() function may still call logoff again if the socket thread has not yet
|
||||||
|
cleared the _connected flag.
|
||||||
|
|
||||||
|
A class should be made for each manager action rather than having a function in
|
||||||
|
a manager class. The manager class should be adapted to have a send method
|
||||||
|
that know the general format of the classes.
|
||||||
|
|
||||||
|
## vim: set fo=awlq:
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
If upgrading from...
|
||||||
|
|
||||||
|
0.1.0:
|
||||||
|
* agi.get_variable no longer throws an exception, instead it returns an
|
||||||
|
empty string when a channel variable is not set.
|
||||||
|
* manager.quit() has be renamed to manager.close()
|
|
@ -0,0 +1,14 @@
|
||||||
|
""" pyst - A set of interfaces and libraries to allow programming of asterisk from python.
|
||||||
|
|
||||||
|
The pyst project includes several python modules to assist in programming
|
||||||
|
asterisk with python:
|
||||||
|
|
||||||
|
agi - python wrapper for agi
|
||||||
|
agitb - a module to assist in agi debugging, like cgitb
|
||||||
|
config - a module for parsing asterisk config files
|
||||||
|
manager - a module for interacting with the asterisk manager interface
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ['agi', 'agitb', 'config', 'manager']
|
||||||
|
|
|
@ -0,0 +1,666 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
# vim: set et sw=4:
|
||||||
|
"""agi
|
||||||
|
|
||||||
|
This module contains functions and classes to implment AGI scripts in python.
|
||||||
|
pyvr
|
||||||
|
|
||||||
|
{'agi_callerid' : 'mars.putland.int',
|
||||||
|
'agi_channel' : 'IAX[kputland@kputland]/119',
|
||||||
|
'agi_context' : 'default',
|
||||||
|
'agi_dnid' : '666',
|
||||||
|
'agi_enhanced' : '0.0',
|
||||||
|
'agi_extension': '666',
|
||||||
|
'agi_language' : 'en',
|
||||||
|
'agi_priority' : '1',
|
||||||
|
'agi_rdnis' : '',
|
||||||
|
'agi_request' : 'pyst',
|
||||||
|
'agi_type' : 'IAX'}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys, pprint, re
|
||||||
|
from types import ListType
|
||||||
|
import signal
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 2000 # 2sec timeout used as default for functions that take timeouts
|
||||||
|
DEFAULT_RECORD = 20000 # 20sec record time
|
||||||
|
|
||||||
|
re_code = re.compile(r'(^\d*)\s*(.*)')
|
||||||
|
re_kv = re.compile(r'(?P<key>\w+)=(?P<value>[^\s]+)\s*(?:\((?P<data>.*)\))*')
|
||||||
|
|
||||||
|
class AGIException(Exception): pass
|
||||||
|
class AGIError(AGIException): pass
|
||||||
|
|
||||||
|
class AGIUnknownError(AGIError): pass
|
||||||
|
|
||||||
|
class AGIAppError(AGIError): pass
|
||||||
|
|
||||||
|
# there are several different types of hangups we can detect
|
||||||
|
# they all are derrived from AGIHangup
|
||||||
|
class AGIHangup(AGIAppError): pass
|
||||||
|
class AGISIGHUPHangup(AGIHangup): pass
|
||||||
|
class AGISIGPIPEHangup(AGIHangup): pass
|
||||||
|
class AGIResultHangup(AGIHangup): pass
|
||||||
|
|
||||||
|
class AGIDBError(AGIAppError): pass
|
||||||
|
|
||||||
|
class AGIUsageError(AGIError): pass
|
||||||
|
class AGIInvalidCommand(AGIError): pass
|
||||||
|
|
||||||
|
class AGI:
|
||||||
|
"""
|
||||||
|
This class encapsulates communication between Asterisk an a python script.
|
||||||
|
It handles encoding commands to Asterisk and parsing responses from
|
||||||
|
Asterisk.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._got_sighup = False
|
||||||
|
signal.signal(signal.SIGHUP, self._handle_sighup) # handle SIGHUP
|
||||||
|
sys.stderr.write('ARGS: ')
|
||||||
|
sys.stderr.write(str(sys.argv))
|
||||||
|
sys.stderr.write('\n')
|
||||||
|
self.env = {}
|
||||||
|
self._get_agi_env()
|
||||||
|
|
||||||
|
def _get_agi_env(self):
|
||||||
|
while 1:
|
||||||
|
line = sys.stdin.readline().strip()
|
||||||
|
sys.stderr.write('ENV LINE: ')
|
||||||
|
sys.stderr.write(line)
|
||||||
|
sys.stderr.write('\n')
|
||||||
|
if line == '':
|
||||||
|
#blank line signals end
|
||||||
|
break
|
||||||
|
key,data = line.split(':')[0], ':'.join(line.split(':')[1:])
|
||||||
|
key = key.strip()
|
||||||
|
data = data.strip()
|
||||||
|
if key <> '':
|
||||||
|
self.env[key] = data
|
||||||
|
sys.stderr.write('class AGI: self.env = ')
|
||||||
|
sys.stderr.write(pprint.pformat(self.env))
|
||||||
|
sys.stderr.write('\n')
|
||||||
|
|
||||||
|
def _quote(self, string):
|
||||||
|
return ''.join(['"', str(string), '"'])
|
||||||
|
|
||||||
|
def _handle_sighup(self, signum, frame):
|
||||||
|
"""Handle the SIGHUP signal"""
|
||||||
|
self._got_sighup = True
|
||||||
|
|
||||||
|
def test_hangup(self):
|
||||||
|
"""This function throws AGIHangup if we have recieved a SIGHUP"""
|
||||||
|
if self._got_sighup:
|
||||||
|
raise AGISIGHUPHangup("Received SIGHUP from Asterisk")
|
||||||
|
|
||||||
|
def execute(self, command, *args):
|
||||||
|
self.test_hangup()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.send_command(command, *args)
|
||||||
|
return self.get_result()
|
||||||
|
except IOError,e:
|
||||||
|
if e.errno == 32:
|
||||||
|
# Broken Pipe * let us go
|
||||||
|
raise AGISIGPIPEHangup("Received SIGPIPE")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send_command(self, command, *args):
|
||||||
|
"""Send a command to Asterisk"""
|
||||||
|
command = command.strip()
|
||||||
|
command = '%s %s' % (command, ' '.join(map(str,args)))
|
||||||
|
command = command.strip()
|
||||||
|
if command[-1] != '\n':
|
||||||
|
command += '\n'
|
||||||
|
sys.stderr.write(' COMMAND: %s' % command)
|
||||||
|
sys.stdout.write(command)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def get_result(self, stdin=sys.stdin):
|
||||||
|
"""Read the result of a command from Asterisk"""
|
||||||
|
code = 0
|
||||||
|
result = {'result':('','')}
|
||||||
|
line = stdin.readline().strip()
|
||||||
|
sys.stderr.write(' RESULT_LINE: %s\n' % line)
|
||||||
|
m = re_code.search(line)
|
||||||
|
if m:
|
||||||
|
code, response = m.groups()
|
||||||
|
code = int(code)
|
||||||
|
|
||||||
|
if code == 200:
|
||||||
|
for key,value,data in re_kv.findall(response):
|
||||||
|
result[key] = (value, data)
|
||||||
|
|
||||||
|
# If user hangs up... we get 'hangup' in the data
|
||||||
|
if data == 'hangup':
|
||||||
|
raise AGIResultHangup("User hungup during execution")
|
||||||
|
|
||||||
|
if key == 'result' and value == '-1':
|
||||||
|
raise AGIAppError("Error executing application, or hangup")
|
||||||
|
|
||||||
|
sys.stderr.write(' RESULT_DICT: %s\n' % pprint.pformat(result))
|
||||||
|
return result
|
||||||
|
elif code == 510:
|
||||||
|
raise AGIInvalidCommand(response)
|
||||||
|
elif code == 520:
|
||||||
|
usage = [line]
|
||||||
|
line = stdin.readline().strip()
|
||||||
|
while line[:3] != '520':
|
||||||
|
usage.append(line)
|
||||||
|
line = stdin.readline().strip()
|
||||||
|
usage.append(line)
|
||||||
|
usage = '%s\n' % '\n'.join(usage)
|
||||||
|
raise AGIUsageError(usage)
|
||||||
|
else:
|
||||||
|
raise AGIUnknownError(code, 'Unhandled code or undefined response')
|
||||||
|
|
||||||
|
def _process_digit_list(self, digits):
|
||||||
|
if type(digits) == ListType:
|
||||||
|
digits = ''.join(map(str, digits))
|
||||||
|
return self._quote(digits)
|
||||||
|
|
||||||
|
def answer(self):
|
||||||
|
"""agi.answer() --> None
|
||||||
|
Answer channel if not already in answer state.
|
||||||
|
"""
|
||||||
|
self.execute('ANSWER')['result'][0]
|
||||||
|
|
||||||
|
def wait_for_digit(self, timeout=DEFAULT_TIMEOUT):
|
||||||
|
"""agi.wait_for_digit(timeout=DEFAULT_TIMEOUT) --> digit
|
||||||
|
Waits for up to 'timeout' milliseconds for a channel to receive a DTMF
|
||||||
|
digit. Returns digit dialed
|
||||||
|
Throws AGIError on channel falure
|
||||||
|
"""
|
||||||
|
res = self.execute('WAIT FOR DIGIT', timeout)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except ValueError:
|
||||||
|
raise AGIError('Unable to convert result to digit: %s' % res)
|
||||||
|
|
||||||
|
def send_text(self, text=''):
|
||||||
|
"""agi.send_text(text='') --> None
|
||||||
|
Sends the given text on a channel. Most channels do not support the
|
||||||
|
transmission of text.
|
||||||
|
Throws AGIError on error/hangup
|
||||||
|
"""
|
||||||
|
self.execute('SEND TEXT', self._quote(text))['result'][0]
|
||||||
|
|
||||||
|
def receive_char(self, timeout=DEFAULT_TIMEOUT):
|
||||||
|
"""agi.receive_char(timeout=DEFAULT_TIMEOUT) --> chr
|
||||||
|
Receives a character of text on a channel. Specify timeout to be the
|
||||||
|
maximum time to wait for input in milliseconds, or 0 for infinite. Most channels
|
||||||
|
do not support the reception of text.
|
||||||
|
"""
|
||||||
|
res = self.execute('RECEIVE CHAR', timeout)['result'][0]
|
||||||
|
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def tdd_mode(self, mode='off'):
|
||||||
|
"""agi.tdd_mode(mode='on'|'off') --> None
|
||||||
|
Enable/Disable TDD transmission/reception on a channel.
|
||||||
|
Throws AGIAppError if channel is not TDD-capable.
|
||||||
|
"""
|
||||||
|
res = self.execute('TDD MODE', mode)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
raise AGIAppError('Channel %s is not TDD-capable')
|
||||||
|
|
||||||
|
def stream_file(self, filename, escape_digits='', sample_offset=0):
|
||||||
|
"""agi.stream_file(filename, escape_digits='', sample_offset=0) --> digit
|
||||||
|
Send the given file, allowing playback to be interrupted by the given
|
||||||
|
digits, if any. escape_digits is a string '12345' or a list of
|
||||||
|
ints [1,2,3,4,5] or strings ['1','2','3'] or mixed [1,'2',3,'4']
|
||||||
|
If sample offset is provided then the audio will seek to sample
|
||||||
|
offset before play starts. Returns digit if one was pressed.
|
||||||
|
Throws AGIError if the channel was disconnected. Remember, the file
|
||||||
|
extension must not be included in the filename.
|
||||||
|
"""
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
response = self.execute('STREAM FILE', filename, escape_digits, sample_offset)
|
||||||
|
res = response['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def control_stream_file(self, filename, escape_digits='', skipms=3000, fwd='', rew='', pause=''):
|
||||||
|
"""
|
||||||
|
Send the given file, allowing playback to be interrupted by the given
|
||||||
|
digits, if any. escape_digits is a string '12345' or a list of
|
||||||
|
ints [1,2,3,4,5] or strings ['1','2','3'] or mixed [1,'2',3,'4']
|
||||||
|
If sample offset is provided then the audio will seek to sample
|
||||||
|
offset before play starts. Returns digit if one was pressed.
|
||||||
|
Throws AGIError if the channel was disconnected. Remember, the file
|
||||||
|
extension must not be included in the filename.
|
||||||
|
"""
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
response = self.execute('CONTROL STREAM FILE', self._quote(filename), escape_digits, self._quote(skipms), self._quote(fwd), self._quote(rew), self._quote(pause))
|
||||||
|
res = response['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def send_image(self, filename):
|
||||||
|
"""agi.send_image(filename) --> None
|
||||||
|
Sends the given image on a channel. Most channels do not support the
|
||||||
|
transmission of images. Image names should not include extensions.
|
||||||
|
Throws AGIError on channel failure
|
||||||
|
"""
|
||||||
|
res = self.execute('SEND IMAGE', filename)['result'][0]
|
||||||
|
if res != '0':
|
||||||
|
raise AGIAppError('Channel falure on channel %s' % self.env.get('agi_channel','UNKNOWN'))
|
||||||
|
|
||||||
|
def say_digits(self, digits, escape_digits=''):
|
||||||
|
"""agi.say_digits(digits, escape_digits='') --> digit
|
||||||
|
Say a given digit string, returning early if any of the given DTMF digits
|
||||||
|
are received on the channel.
|
||||||
|
Throws AGIError on channel failure
|
||||||
|
"""
|
||||||
|
digits = self._process_digit_list(digits)
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
res = self.execute('SAY DIGITS', digits, escape_digits)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def say_number(self, number, escape_digits=''):
|
||||||
|
"""agi.say_number(number, escape_digits='') --> digit
|
||||||
|
Say a given digit string, returning early if any of the given DTMF digits
|
||||||
|
are received on the channel.
|
||||||
|
Throws AGIError on channel failure
|
||||||
|
"""
|
||||||
|
number = self._process_digit_list(number)
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
res = self.execute('SAY NUMBER', number, escape_digits)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def say_alpha(self, characters, escape_digits=''):
|
||||||
|
"""agi.say_alpha(string, escape_digits='') --> digit
|
||||||
|
Say a given character string, returning early if any of the given DTMF
|
||||||
|
digits are received on the channel.
|
||||||
|
Throws AGIError on channel failure
|
||||||
|
"""
|
||||||
|
characters = self._process_digit_list(characters)
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
res = self.execute('SAY ALPHA', characters, escape_digits)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def say_phonetic(self, characters, escape_digits=''):
|
||||||
|
"""agi.say_phonetic(string, escape_digits='') --> digit
|
||||||
|
Phonetically say a given character string, returning early if any of
|
||||||
|
the given DTMF digits are received on the channel.
|
||||||
|
Throws AGIError on channel failure
|
||||||
|
"""
|
||||||
|
characters = self._process_digit_list(characters)
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
res = self.execute('SAY PHONETIC', characters, escape_digits)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def say_date(self, seconds, escape_digits=''):
|
||||||
|
"""agi.say_date(seconds, escape_digits='') --> digit
|
||||||
|
Say a given date, returning early if any of the given DTMF digits are
|
||||||
|
pressed. The date should be in seconds since the UNIX Epoch (Jan 1, 1970 00:00:00)
|
||||||
|
"""
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
res = self.execute('SAY DATE', seconds, escape_digits)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def say_time(self, seconds, escape_digits=''):
|
||||||
|
"""agi.say_time(seconds, escape_digits='') --> digit
|
||||||
|
Say a given time, returning early if any of the given DTMF digits are
|
||||||
|
pressed. The time should be in seconds since the UNIX Epoch (Jan 1, 1970 00:00:00)
|
||||||
|
"""
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
res = self.execute('SAY TIME', seconds, escape_digits)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def say_datetime(self, seconds, escape_digits='', format='', zone=''):
|
||||||
|
"""agi.say_datetime(seconds, escape_digits='', format='', zone='') --> digit
|
||||||
|
Say a given date in the format specfied (see voicemail.conf), returning
|
||||||
|
early if any of the given DTMF digits are pressed. The date should be
|
||||||
|
in seconds since the UNIX Epoch (Jan 1, 1970 00:00:00).
|
||||||
|
"""
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
if format: format = self._quote(format)
|
||||||
|
res = self.execute('SAY DATETIME', seconds, escape_digits, format, zone)['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def get_data(self, filename, timeout=DEFAULT_TIMEOUT, max_digits=255):
|
||||||
|
"""agi.get_data(filename, timeout=DEFAULT_TIMEOUT, max_digits=255) --> digits
|
||||||
|
Stream the given file and receive dialed digits
|
||||||
|
"""
|
||||||
|
result = self.execute('GET DATA', filename, timeout, max_digits)
|
||||||
|
res, value = result['result']
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_option(self, filename, escape_digits='', timeout=0):
|
||||||
|
"""agi.get_option(filename, escape_digits='', timeout=0) --> digit
|
||||||
|
Send the given file, allowing playback to be interrupted by the given
|
||||||
|
digits, if any. escape_digits is a string '12345' or a list of
|
||||||
|
ints [1,2,3,4,5] or strings ['1','2','3'] or mixed [1,'2',3,'4']
|
||||||
|
Returns digit if one was pressed.
|
||||||
|
Throws AGIError if the channel was disconnected. Remember, the file
|
||||||
|
extension must not be included in the filename.
|
||||||
|
"""
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
if timeout:
|
||||||
|
response = self.execute('GET OPTION', filename, escape_digits, timeout)
|
||||||
|
else:
|
||||||
|
response = self.execute('GET OPTION', filename, escape_digits)
|
||||||
|
|
||||||
|
res = response['result'][0]
|
||||||
|
if res == '0':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to char: %s' % res)
|
||||||
|
|
||||||
|
def set_context(self, context):
|
||||||
|
"""agi.set_context(context)
|
||||||
|
Sets the context for continuation upon exiting the application.
|
||||||
|
No error appears to be produced. Does not set exten or priority
|
||||||
|
Use at your own risk. Ensure that you specify a valid context.
|
||||||
|
"""
|
||||||
|
self.execute('SET CONTEXT', context)
|
||||||
|
|
||||||
|
def set_extension(self, extension):
|
||||||
|
"""agi.set_extension(extension)
|
||||||
|
Sets the extension for continuation upon exiting the application.
|
||||||
|
No error appears to be produced. Does not set context or priority
|
||||||
|
Use at your own risk. Ensure that you specify a valid extension.
|
||||||
|
"""
|
||||||
|
self.execute('SET EXTENSION', extension)
|
||||||
|
|
||||||
|
def set_priority(self, priority):
|
||||||
|
"""agi.set_priority(priority)
|
||||||
|
Sets the priority for continuation upon exiting the application.
|
||||||
|
No error appears to be produced. Does not set exten or context
|
||||||
|
Use at your own risk. Ensure that you specify a valid priority.
|
||||||
|
"""
|
||||||
|
self.execute('set priority', priority)
|
||||||
|
|
||||||
|
def goto_on_exit(self, context='', extension='', priority=''):
|
||||||
|
context = context or self.env['agi_context']
|
||||||
|
extension = extension or self.env['agi_extension']
|
||||||
|
priority = priority or self.env['agi_priority']
|
||||||
|
self.set_context(context)
|
||||||
|
self.set_extension(extension)
|
||||||
|
self.set_priority(priority)
|
||||||
|
|
||||||
|
def record_file(self, filename, format='gsm', escape_digits='#', timeout=DEFAULT_RECORD, offset=0, beep='beep'):
|
||||||
|
"""agi.record_file(filename, format, escape_digits, timeout=DEFAULT_TIMEOUT, offset=0, beep='beep') --> None
|
||||||
|
Record to a file until a given dtmf digit in the sequence is received
|
||||||
|
The format will specify what kind of file will be recorded. The timeout
|
||||||
|
is the maximum record time in milliseconds, or -1 for no timeout. Offset
|
||||||
|
samples is optional, and if provided will seek to the offset without
|
||||||
|
exceeding the end of the file
|
||||||
|
"""
|
||||||
|
escape_digits = self._process_digit_list(escape_digits)
|
||||||
|
res = self.execute('RECORD FILE', self._quote(filename), format, escape_digits, timeout, offset, beep)['result'][0]
|
||||||
|
try:
|
||||||
|
return chr(int(res))
|
||||||
|
except:
|
||||||
|
raise AGIError('Unable to convert result to digit: %s' % res)
|
||||||
|
|
||||||
|
def set_autohangup(self, secs):
|
||||||
|
"""agi.set_autohangup(secs) --> None
|
||||||
|
Cause the channel to automatically hangup at <time> seconds in the
|
||||||
|
future. Of course it can be hungup before then as well. Setting to
|
||||||
|
0 will cause the autohangup feature to be disabled on this channel.
|
||||||
|
"""
|
||||||
|
self.execute('SET AUTOHANGUP', time)
|
||||||
|
|
||||||
|
def hangup(self, channel=''):
|
||||||
|
"""agi.hangup(channel='')
|
||||||
|
Hangs up the specified channel.
|
||||||
|
If no channel name is given, hangs up the current channel
|
||||||
|
"""
|
||||||
|
self.execute('HANGUP', channel)
|
||||||
|
|
||||||
|
def appexec(self, application, options=''):
|
||||||
|
"""agi.appexec(application, options='')
|
||||||
|
Executes <application> with given <options>.
|
||||||
|
Returns whatever the application returns, or -2 on failure to find
|
||||||
|
application
|
||||||
|
"""
|
||||||
|
result = self.execute('EXEC', application, self._quote(options))
|
||||||
|
res = result['result'][0]
|
||||||
|
if res == '-2':
|
||||||
|
raise AGIAppError('Unable to find application: %s' % application)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def set_callerid(self, number):
|
||||||
|
"""agi.set_callerid(number) --> None
|
||||||
|
Changes the callerid of the current channel.
|
||||||
|
"""
|
||||||
|
self.execute('SET CALLERID', number)
|
||||||
|
|
||||||
|
def channel_status(self, channel=''):
|
||||||
|
"""agi.channel_status(channel='') --> int
|
||||||
|
Returns the status of the specified channel. If no channel name is
|
||||||
|
given the returns the status of the current channel.
|
||||||
|
|
||||||
|
Return values:
|
||||||
|
0 Channel is down and available
|
||||||
|
1 Channel is down, but reserved
|
||||||
|
2 Channel is off hook
|
||||||
|
3 Digits (or equivalent) have been dialed
|
||||||
|
4 Line is ringing
|
||||||
|
5 Remote end is ringing
|
||||||
|
6 Line is up
|
||||||
|
7 Line is busy
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = self.execute('CHANNEL STATUS', channel)
|
||||||
|
except AGIHangup:
|
||||||
|
raise
|
||||||
|
except AGIAppError:
|
||||||
|
result = {'result': ('-1','')}
|
||||||
|
|
||||||
|
return int(result['result'][0])
|
||||||
|
|
||||||
|
def set_variable(self, name, value):
|
||||||
|
"""Set a channel variable.
|
||||||
|
"""
|
||||||
|
self.execute('SET VARIABLE', self._quote(name), self._quote(value))
|
||||||
|
|
||||||
|
def get_variable(self, name):
|
||||||
|
"""Get a channel variable.
|
||||||
|
|
||||||
|
This function returns the value of the indicated channel variable. If
|
||||||
|
the variable is not set, an empty string is returned.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = self.execute('GET VARIABLE', self._quote(name))
|
||||||
|
except AGIResultHangup:
|
||||||
|
result = {'result': ('1', 'hangup')}
|
||||||
|
|
||||||
|
res, value = result['result']
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_full_variable(self, name, channel = None):
|
||||||
|
"""Get a channel variable.
|
||||||
|
|
||||||
|
This function returns the value of the indicated channel variable. If
|
||||||
|
the variable is not set, an empty string is returned.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if channel:
|
||||||
|
result = self.execute('GET FULL VARIABLE', self._quote(name), self._quote(channel))
|
||||||
|
else:
|
||||||
|
result = self.execute('GET FULL VARIABLE', self._quote(name))
|
||||||
|
|
||||||
|
except AGIResultHangup:
|
||||||
|
result = {'result': ('1', 'hangup')}
|
||||||
|
|
||||||
|
res, value = result['result']
|
||||||
|
return value
|
||||||
|
|
||||||
|
def verbose(self, message, level=1):
|
||||||
|
"""agi.verbose(message='', level=1) --> None
|
||||||
|
Sends <message> to the console via verbose message system.
|
||||||
|
<level> is the the verbose level (1-4)
|
||||||
|
"""
|
||||||
|
self.execute('VERBOSE', self._quote(message), level)
|
||||||
|
|
||||||
|
def database_get(self, family, key):
|
||||||
|
"""agi.database_get(family, key) --> str
|
||||||
|
Retrieves an entry in the Asterisk database for a given family and key.
|
||||||
|
Returns 0 if <key> is not set. Returns 1 if <key>
|
||||||
|
is set and returns the variable in parenthesis
|
||||||
|
example return code: 200 result=1 (testvariable)
|
||||||
|
"""
|
||||||
|
family = '"%s"' % family
|
||||||
|
key = '"%s"' % key
|
||||||
|
result = self.execute('DATABASE GET', self._quote(family), self._quote(key))
|
||||||
|
res, value = result['result']
|
||||||
|
if res == '0':
|
||||||
|
raise AGIDBError('Key not found in database: family=%s, key=%s' % (family, key))
|
||||||
|
elif res == '1':
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise AGIError('Unknown exception for : family=%s, key=%s, result=%s' % (family, key, pprint.pformat(result)))
|
||||||
|
|
||||||
|
def database_put(self, family, key, value):
|
||||||
|
"""agi.database_put(family, key, value) --> None
|
||||||
|
Adds or updates an entry in the Asterisk database for a
|
||||||
|
given family, key, and value.
|
||||||
|
"""
|
||||||
|
result = self.execute('DATABASE PUT', self._quote(family), self._quote(key), self._quote(value))
|
||||||
|
res, value = result['result']
|
||||||
|
if res == '0':
|
||||||
|
raise AGIDBError('Unable to put vaule in databale: family=%s, key=%s, value=%s' % (family, key, value))
|
||||||
|
|
||||||
|
def database_del(self, family, key):
|
||||||
|
"""agi.database_del(family, key) --> None
|
||||||
|
Deletes an entry in the Asterisk database for a
|
||||||
|
given family and key.
|
||||||
|
"""
|
||||||
|
result = self.execute('DATABASE DEL', self._quote(family), self._quote(key))
|
||||||
|
res, value = result['result']
|
||||||
|
if res == '0':
|
||||||
|
raise AGIDBError('Unable to delete from database: family=%s, key=%s' % (family, key))
|
||||||
|
|
||||||
|
def database_deltree(self, family, key=''):
|
||||||
|
"""agi.database_deltree(family, key='') --> None
|
||||||
|
Deletes a family or specific keytree with in a family
|
||||||
|
in the Asterisk database.
|
||||||
|
"""
|
||||||
|
result = self.execute('DATABASE DELTREE', self._quote(family), self._quote(key))
|
||||||
|
res, value = result['result']
|
||||||
|
if res == '0':
|
||||||
|
raise AGIDBError('Unable to delete tree from database: family=%s, key=%s' % (family, key))
|
||||||
|
|
||||||
|
def noop(self):
|
||||||
|
"""agi.noop() --> None
|
||||||
|
Does nothing
|
||||||
|
"""
|
||||||
|
self.execute('NOOP')
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
agi = AGI()
|
||||||
|
#agi.appexec('festival','Welcome to Klass Technologies. Thank you for calling.')
|
||||||
|
#agi.appexec('festival','This is a test of the text to speech engine.')
|
||||||
|
#agi.appexec('festival','Press 1 for sales ')
|
||||||
|
#agi.appexec('festival','Press 2 for customer support ')
|
||||||
|
#agi.hangup()
|
||||||
|
#agi.goto_on_exit(extension='1234', priority='1')
|
||||||
|
#sys.exit(0)
|
||||||
|
#agi.say_digits('123', [4,'5',6])
|
||||||
|
#agi.say_digits([4,5,6])
|
||||||
|
#agi.say_number('1234')
|
||||||
|
#agi.say_number('01234') # 668
|
||||||
|
#agi.say_number('0xf5') # 245
|
||||||
|
agi.get_data('demo-congrats')
|
||||||
|
agi.hangup()
|
||||||
|
sys.exit(0)
|
||||||
|
#agi.record_file('pyst-test') #FAILS
|
||||||
|
#agi.stream_file('demo-congrats', [1,2,3,4,5,6,7,8,9,0,'#','*'])
|
||||||
|
#agi.appexec('background','demo-congrats')
|
||||||
|
|
||||||
|
try:
|
||||||
|
agi.appexec('backgrounder','demo-congrats')
|
||||||
|
except AGIAppError:
|
||||||
|
sys.stderr.write("Handled exception for missing application backgrounder\n")
|
||||||
|
|
||||||
|
agi.set_variable('foo','bar')
|
||||||
|
agi.get_variable('foo')
|
||||||
|
|
||||||
|
try:
|
||||||
|
agi.get_variable('foobar')
|
||||||
|
except AGIAppError:
|
||||||
|
sys.stderr.write("Handled exception for missing variable foobar\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
agi.database_put('foo', 'bar', 'foobar')
|
||||||
|
agi.database_put('foo', 'baz', 'foobaz')
|
||||||
|
agi.database_put('foo', 'bat', 'foobat')
|
||||||
|
v = agi.database_get('foo', 'bar')
|
||||||
|
sys.stderr.write('DBVALUE foo:bar = %s\n' % v)
|
||||||
|
v = agi.database_get('bar', 'foo')
|
||||||
|
sys.stderr.write('DBVALUE foo:bar = %s\n' % v)
|
||||||
|
agi.database_del('foo', 'bar')
|
||||||
|
agi.database_deltree('foo')
|
||||||
|
except AGIDBError:
|
||||||
|
sys.stderr.write("Handled exception for missing database entry bar:foo\n")
|
||||||
|
|
||||||
|
agi.hangup()
|
|
@ -0,0 +1,216 @@
|
||||||
|
"""More comprehensive traceback formatting for Python scripts.
|
||||||
|
|
||||||
|
To enable this module, do:
|
||||||
|
|
||||||
|
import asterisk.agitb, asterisk.agi
|
||||||
|
asterisk.agitb.enable(display = False, logdir = '/var/log/asterisk/')
|
||||||
|
|
||||||
|
agi = asterisk.agi.AGI()
|
||||||
|
asterisk.agitb.enable(agi, False, '/var/log/asterisk')
|
||||||
|
|
||||||
|
at the top of your script. The optional arguments to enable() are:
|
||||||
|
|
||||||
|
agi - the agi handle to write verbose messages to
|
||||||
|
display - if true, tracebacks are displayed on the asterisk console
|
||||||
|
(used with the agi option)
|
||||||
|
logdir - if set, tracebacks are written to files in this directory
|
||||||
|
context - number of lines of source code to show for each stack frame
|
||||||
|
|
||||||
|
By default, tracebacks are displayed but not saved, and the context is 5 lines.
|
||||||
|
|
||||||
|
You may want to add a logdir if you call agitb.enable() before you have
|
||||||
|
an agi.AGI() handle.
|
||||||
|
|
||||||
|
Alternatively, if you have caught an exception and want agitb to display it
|
||||||
|
for you, call agitb.handler(). The optional argument to handler() is a
|
||||||
|
3-item tuple (etype, evalue, etb) just like the value of sys.exc_info().
|
||||||
|
If you do not pass anything to handler() it will use sys.exc_info().
|
||||||
|
|
||||||
|
This script was adapted from Ka-Ping Yee's cgitb.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = 'Matthew Nicholson'
|
||||||
|
__version__ = '0.1.0'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
__UNDEF__ = [] # a special sentinel object
|
||||||
|
|
||||||
|
def lookup(name, frame, locals):
|
||||||
|
"""Find the value for a given name in the given environment."""
|
||||||
|
if name in locals:
|
||||||
|
return 'local', locals[name]
|
||||||
|
if name in frame.f_globals:
|
||||||
|
return 'global', frame.f_globals[name]
|
||||||
|
if '__builtins__' in frame.f_globals:
|
||||||
|
builtins = frame.f_globals['__builtins__']
|
||||||
|
if type(builtins) is type({}):
|
||||||
|
if name in builtins:
|
||||||
|
return 'builtin', builtins[name]
|
||||||
|
else:
|
||||||
|
if hasattr(builtins, name):
|
||||||
|
return 'builtin', getattr(builtins, name)
|
||||||
|
return None, __UNDEF__
|
||||||
|
|
||||||
|
def scanvars(reader, frame, locals):
|
||||||
|
"""Scan one logical line of Python and look up values of variables used."""
|
||||||
|
import tokenize, keyword
|
||||||
|
vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
|
||||||
|
for ttype, token, start, end, line in tokenize.generate_tokens(reader):
|
||||||
|
if ttype == tokenize.NEWLINE: break
|
||||||
|
if ttype == tokenize.NAME and token not in keyword.kwlist:
|
||||||
|
if lasttoken == '.':
|
||||||
|
if parent is not __UNDEF__:
|
||||||
|
value = getattr(parent, token, __UNDEF__)
|
||||||
|
vars.append((prefix + token, prefix, value))
|
||||||
|
else:
|
||||||
|
where, value = lookup(token, frame, locals)
|
||||||
|
vars.append((token, where, value))
|
||||||
|
elif token == '.':
|
||||||
|
prefix += lasttoken + '.'
|
||||||
|
parent = value
|
||||||
|
else:
|
||||||
|
parent, prefix = None, ''
|
||||||
|
lasttoken = token
|
||||||
|
return vars
|
||||||
|
|
||||||
|
|
||||||
|
def text((etype, evalue, etb), context=5):
|
||||||
|
"""Return a plain text document describing a given traceback."""
|
||||||
|
import os, types, time, traceback, linecache, inspect, pydoc
|
||||||
|
|
||||||
|
if type(etype) is types.ClassType:
|
||||||
|
etype = etype.__name__
|
||||||
|
pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
|
||||||
|
date = time.ctime(time.time())
|
||||||
|
head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + '''
|
||||||
|
A problem occurred in a Python script. Here is the sequence of
|
||||||
|
function calls leading up to the error, in the order they occurred.
|
||||||
|
'''
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
records = inspect.getinnerframes(etb, context)
|
||||||
|
for frame, file, lnum, func, lines, index in records:
|
||||||
|
file = file and os.path.abspath(file) or '?'
|
||||||
|
args, varargs, varkw, locals = inspect.getargvalues(frame)
|
||||||
|
call = ''
|
||||||
|
if func != '?':
|
||||||
|
call = 'in ' + func + \
|
||||||
|
inspect.formatargvalues(args, varargs, varkw, locals,
|
||||||
|
formatvalue=lambda value: '=' + pydoc.text.repr(value))
|
||||||
|
|
||||||
|
highlight = {}
|
||||||
|
def reader(lnum=[lnum]):
|
||||||
|
highlight[lnum[0]] = 1
|
||||||
|
try: return linecache.getline(file, lnum[0])
|
||||||
|
finally: lnum[0] += 1
|
||||||
|
vars = scanvars(reader, frame, locals)
|
||||||
|
|
||||||
|
rows = [' %s %s' % (file, call)]
|
||||||
|
if index is not None:
|
||||||
|
i = lnum - index
|
||||||
|
for line in lines:
|
||||||
|
num = '%5d ' % i
|
||||||
|
rows.append(num+line.rstrip())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
done, dump = {}, []
|
||||||
|
for name, where, value in vars:
|
||||||
|
if name in done: continue
|
||||||
|
done[name] = 1
|
||||||
|
if value is not __UNDEF__:
|
||||||
|
if where == 'global': name = 'global ' + name
|
||||||
|
elif where == 'local': name = name
|
||||||
|
else: name = where + name.split('.')[-1]
|
||||||
|
dump.append('%s = %s' % (name, pydoc.text.repr(value)))
|
||||||
|
else:
|
||||||
|
dump.append(name + ' undefined')
|
||||||
|
|
||||||
|
rows.append('\n'.join(dump))
|
||||||
|
frames.append('\n%s\n' % '\n'.join(rows))
|
||||||
|
|
||||||
|
exception = ['%s: %s' % (str(etype), str(evalue))]
|
||||||
|
if type(evalue) is types.InstanceType:
|
||||||
|
for name in dir(evalue):
|
||||||
|
value = pydoc.text.repr(getattr(evalue, name))
|
||||||
|
exception.append('\n%s%s = %s' % (" "*4, name, value))
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
return head + ''.join(frames) + ''.join(exception) + '''
|
||||||
|
|
||||||
|
The above is a description of an error in a Python program. Here is
|
||||||
|
the original traceback:
|
||||||
|
|
||||||
|
%s
|
||||||
|
''' % ''.join(traceback.format_exception(etype, evalue, etb))
|
||||||
|
|
||||||
|
class Hook:
|
||||||
|
"""A hook to replace sys.excepthook that shows tracebacks in HTML."""
|
||||||
|
|
||||||
|
def __init__(self, display=1, logdir=None, context=5, file=None,
|
||||||
|
agi=None):
|
||||||
|
self.display = display # send tracebacks to browser if true
|
||||||
|
self.logdir = logdir # log tracebacks to files if not None
|
||||||
|
self.context = context # number of source code lines per frame
|
||||||
|
self.file = file or sys.stderr # place to send the output
|
||||||
|
self.agi = agi
|
||||||
|
|
||||||
|
def __call__(self, etype, evalue, etb):
|
||||||
|
self.handle((etype, evalue, etb))
|
||||||
|
|
||||||
|
def handle(self, info=None):
|
||||||
|
info = info or sys.exc_info()
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = text(info, self.context)
|
||||||
|
except: # just in case something goes wrong
|
||||||
|
import traceback
|
||||||
|
doc = ''.join(traceback.format_exception(*info))
|
||||||
|
|
||||||
|
if self.display:
|
||||||
|
if self.agi: # print to agi
|
||||||
|
for line in doc.split('\n'):
|
||||||
|
self.agi.verbose(line, 4)
|
||||||
|
else:
|
||||||
|
self.file.write(doc + '\n')
|
||||||
|
|
||||||
|
if self.agi:
|
||||||
|
self.agi.verbose('A problem occured in a python script', 4)
|
||||||
|
else:
|
||||||
|
self.file.write('A problem occured in a python script\n')
|
||||||
|
|
||||||
|
if self.logdir is not None:
|
||||||
|
import os, tempfile
|
||||||
|
(fd, path) = tempfile.mkstemp(suffix='.txt', dir=self.logdir)
|
||||||
|
try:
|
||||||
|
file = os.fdopen(fd, 'w')
|
||||||
|
file.write(doc)
|
||||||
|
file.close()
|
||||||
|
msg = '%s contains the description of this error.' % path
|
||||||
|
except:
|
||||||
|
msg = 'Tried to save traceback to %s, but failed.' % path
|
||||||
|
|
||||||
|
if self.agi:
|
||||||
|
self.agi.verbose(msg, 4)
|
||||||
|
else:
|
||||||
|
self.file.write(msg + '\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.file.flush()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
|
||||||
|
handler = Hook().handle
|
||||||
|
def enable(agi=None, display=1, logdir=None, context=5):
|
||||||
|
"""Install an exception handler that formats tracebacks as HTML.
|
||||||
|
|
||||||
|
The optional argument 'display' can be set to 0 to suppress sending the
|
||||||
|
traceback to the browser, and 'logdir' can be set to a directory to cause
|
||||||
|
tracebacks to be written to files there."""
|
||||||
|
except_hook = Hook(display=display, logdir=logdir,
|
||||||
|
context=context, agi=agi)
|
||||||
|
sys.excepthook = except_hook
|
||||||
|
|
||||||
|
global handler
|
||||||
|
handler = except_hook.handle
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: set expandtab:
|
||||||
|
"""
|
||||||
|
Parse Asterisk configuration files.
|
||||||
|
|
||||||
|
This module provides parsing functionality for asterisk config files.
|
||||||
|
|
||||||
|
import asterisk.config
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# load and parse the config file
|
||||||
|
try:
|
||||||
|
config = asterisk.config.Config('/etc/asterisk/extensions.conf')
|
||||||
|
except asterisk.config.ParseError, (line, reason):
|
||||||
|
print "Parse Error line: %s: %s" % (line, reason)
|
||||||
|
sys.exit(1)
|
||||||
|
except IOError, reason:
|
||||||
|
print "Error opening file: %s" % reason
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# print our parsed output
|
||||||
|
for category in config.categories:
|
||||||
|
print '[%s]' % category.name # print the current category
|
||||||
|
|
||||||
|
for item in category.items:
|
||||||
|
print ' %s = %s' % (item.name, item.value)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class ParseError(Exception): pass
|
||||||
|
|
||||||
|
class Line(object):
|
||||||
|
def __init__(self, line, number):
|
||||||
|
self.line = ''
|
||||||
|
self.comment = ''
|
||||||
|
line = line.strip() # I guess we don't preserve indentation
|
||||||
|
self.number = number
|
||||||
|
parts = line.split(';')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
self.line = parts[0].strip()
|
||||||
|
self.comment = ';'.join(parts[1:]) #Just in case the comment contained ';'
|
||||||
|
else:
|
||||||
|
self.line = line
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.get_line()
|
||||||
|
|
||||||
|
def get_line(self):
|
||||||
|
if self.comment and self.line:
|
||||||
|
return '%s\t;%s' % (self.line, self.comment)
|
||||||
|
elif self.comment and not self.line:
|
||||||
|
return ';%s' % self.comment
|
||||||
|
return self.line
|
||||||
|
|
||||||
|
|
||||||
|
class Category(Line):
|
||||||
|
def __init__(self, line='', num=-1, name=None):
|
||||||
|
Line.__init__(self, line, num)
|
||||||
|
if self.line:
|
||||||
|
if (self.line[0] != '[' or self.line[-1] != ']'):
|
||||||
|
raise ParseError(self.number, "Missing '[' or ']' in category definition")
|
||||||
|
self.name = self.line[1:-1]
|
||||||
|
elif name:
|
||||||
|
self.name = name
|
||||||
|
else:
|
||||||
|
raise Exception("Must provide name or line representing a category")
|
||||||
|
|
||||||
|
self.items = []
|
||||||
|
self.comments = []
|
||||||
|
|
||||||
|
def get_line(self):
|
||||||
|
if self.comment:
|
||||||
|
return '[%s]\t;%s' % (self.name, self.comment)
|
||||||
|
return '[%s]' % self.name
|
||||||
|
|
||||||
|
def append(self, item):
|
||||||
|
self.items.append(item)
|
||||||
|
|
||||||
|
def insert(self, index, item):
|
||||||
|
self.items.insert(index, item)
|
||||||
|
|
||||||
|
def pop(self, index=-1):
|
||||||
|
self.items.pop(index)
|
||||||
|
|
||||||
|
def remove(self, item):
|
||||||
|
self.items.remove(item)
|
||||||
|
|
||||||
|
|
||||||
|
class Item(Line):
|
||||||
|
def __init__(self, line='', num=-1, name=None, value=None):
|
||||||
|
Line.__init__(self, line, num)
|
||||||
|
self.style = ''
|
||||||
|
if self.line:
|
||||||
|
self.parse()
|
||||||
|
elif (name and value):
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
else:
|
||||||
|
raise Exception("Must provide name or value representing an item")
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
try:
|
||||||
|
name, value = self.line.split('=', 1)
|
||||||
|
except ValueError:
|
||||||
|
if self.line.strip()[-1] == ']':
|
||||||
|
raise ParseError(self.number, "Category name missing '['")
|
||||||
|
else:
|
||||||
|
raise ParseError(self.number, "Item must be in name = value pairs")
|
||||||
|
|
||||||
|
if value and value[0] == '>':
|
||||||
|
self.style = '>' #preserve the style of the original
|
||||||
|
value = value[1:].strip()
|
||||||
|
self.name = name.strip()
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def get_line(self):
|
||||||
|
if self.comment:
|
||||||
|
return '%s =%s %s\t;%s' % (self.name, self.style, self.value, self.comment)
|
||||||
|
return '%s =%s %s' % (self.name, self.style, self.value)
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.filename = filename
|
||||||
|
self.raw_lines = [] # Holds the raw strings
|
||||||
|
self.lines = [] # Holds things in order
|
||||||
|
self.categories = []
|
||||||
|
|
||||||
|
# load and parse the file
|
||||||
|
self.load()
|
||||||
|
self.parse()
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.raw_lines = open(self.filename).readlines()
|
||||||
|
#try:
|
||||||
|
#self.raw_lines = open(self.filename).readlines()
|
||||||
|
#except IOError:
|
||||||
|
#sys.stderr.write('WARNING: error opening filename: %s No data read. Starting new file?' % self.filename)
|
||||||
|
#self.raw_lines = []
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
cat = None
|
||||||
|
num = 0
|
||||||
|
for line in self.raw_lines:
|
||||||
|
num += 1
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line[0] == ';':
|
||||||
|
item = Line(line or '', num)
|
||||||
|
self.lines.append(item)
|
||||||
|
if cat: cat.comments.append(item)
|
||||||
|
continue
|
||||||
|
elif line[0] == '[':
|
||||||
|
cat = Category(line, num)
|
||||||
|
self.lines.append(cat)
|
||||||
|
self.categories.append(cat)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
item = Item(line, num)
|
||||||
|
self.lines.append(item)
|
||||||
|
if cat: cat.append(item)
|
||||||
|
continue
|
||||||
|
|
|
@ -0,0 +1,597 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: set expandtab shiftwidth=4:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Python Interface for Asterisk Manager
|
||||||
|
|
||||||
|
This module provides a Python API for interfacing with the asterisk manager.
|
||||||
|
|
||||||
|
import asterisk.manager
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def handle_shutdown(event, manager):
|
||||||
|
print "Recieved shutdown event"
|
||||||
|
manager.close()
|
||||||
|
# we could analize the event and reconnect here
|
||||||
|
|
||||||
|
def handle_event(event, manager):
|
||||||
|
print "Recieved event: %s" % event.name
|
||||||
|
|
||||||
|
manager = asterisk.manager.Manager()
|
||||||
|
try:
|
||||||
|
# connect to the manager
|
||||||
|
try:
|
||||||
|
manager.connect('host')
|
||||||
|
manager.login('user', 'secret')
|
||||||
|
|
||||||
|
# register some callbacks
|
||||||
|
manager.register_event('Shutdown', handle_shutdown) # shutdown
|
||||||
|
manager.register_event('*', handle_event) # catch all
|
||||||
|
|
||||||
|
# get a status report
|
||||||
|
response = manager.status()
|
||||||
|
|
||||||
|
manager.logoff()
|
||||||
|
except asterisk.manager.ManagerSocketException, (errno, reason):
|
||||||
|
print "Error connecting to the manager: %s" % reason
|
||||||
|
sys.exit(1)
|
||||||
|
except asterisk.manager.ManagerAuthException, reason:
|
||||||
|
print "Error logging in to the manager: %s" % reason
|
||||||
|
sys.exit(1)
|
||||||
|
except asterisk.manager.ManagerException, reason:
|
||||||
|
print "Error: %s" % reason
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# remember to clean up
|
||||||
|
manager.close()
|
||||||
|
|
||||||
|
Remember all header, response, and event names are case sensitive.
|
||||||
|
|
||||||
|
Not all manager actions are implmented as of yet, feel free to add them
|
||||||
|
and submit patches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys,os
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import Queue
|
||||||
|
import re
|
||||||
|
from cStringIO import StringIO
|
||||||
|
from types import *
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
EOL = '\r\n'
|
||||||
|
|
||||||
|
class ManagerMsg(object):
|
||||||
|
"""A manager interface message"""
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response # the raw response, straight from the horse's mouth
|
||||||
|
self.data = ''
|
||||||
|
self.headers = {}
|
||||||
|
|
||||||
|
# parse the response
|
||||||
|
self.parse(response)
|
||||||
|
|
||||||
|
if not self.headers:
|
||||||
|
# Bad app not returning any headers. Let's fake it
|
||||||
|
# this could also be the inital greeting
|
||||||
|
self.headers['Response'] = 'Generated Header'
|
||||||
|
# 'Response:'
|
||||||
|
|
||||||
|
def parse(self, response):
|
||||||
|
"""Parse a manager message"""
|
||||||
|
response.seek(0)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
|
||||||
|
# read the response line by line
|
||||||
|
for line in response.readlines():
|
||||||
|
line = line.rstrip() # strip trailing whitespace
|
||||||
|
|
||||||
|
if not line: continue # don't process if this is not a message
|
||||||
|
|
||||||
|
# locate the ':' in our message, if there is one
|
||||||
|
if line.find(':') > -1:
|
||||||
|
item = [x.strip() for x in line.split(':',1)]
|
||||||
|
|
||||||
|
# if this is a header
|
||||||
|
if len(item) == 2:
|
||||||
|
# store the header
|
||||||
|
self.headers[item[0]] = item[1]
|
||||||
|
# otherwise it is just plain data
|
||||||
|
else:
|
||||||
|
data.append(line)
|
||||||
|
# if there was no ':', then we have data
|
||||||
|
else:
|
||||||
|
data.append(line)
|
||||||
|
|
||||||
|
# store the data
|
||||||
|
self.data = '%s\n' % '\n'.join(data)
|
||||||
|
|
||||||
|
def has_header(self, hname):
|
||||||
|
"""Check for a header"""
|
||||||
|
return self.headers.has_key(hname)
|
||||||
|
|
||||||
|
def get_header(self, hname):
|
||||||
|
"""Return the specfied header"""
|
||||||
|
return self.headers[hname]
|
||||||
|
|
||||||
|
def __getitem__(self, hname):
|
||||||
|
"""Return the specfied header"""
|
||||||
|
return self.headers[hname]
|
||||||
|
def __repr__(self):
|
||||||
|
return self.headers['Response']
|
||||||
|
|
||||||
|
|
||||||
|
class Event(object):
|
||||||
|
"""Manager interface Events, __init__ expects and 'Event' message"""
|
||||||
|
def __init__(self, message):
|
||||||
|
|
||||||
|
# store all of the event data
|
||||||
|
self.message = message
|
||||||
|
self.data = message.data
|
||||||
|
self.headers = message.headers
|
||||||
|
|
||||||
|
# if this is not an event message we have a problem
|
||||||
|
if not message.has_header('Event'):
|
||||||
|
raise ManagerException('Trying to create event from non event message')
|
||||||
|
|
||||||
|
# get the event name
|
||||||
|
self.name = message.get_header('Event')
|
||||||
|
|
||||||
|
def has_header(self, hname):
|
||||||
|
"""Check for a header"""
|
||||||
|
return self.headers.has_key(hname)
|
||||||
|
|
||||||
|
def get_header(self, hname):
|
||||||
|
"""Return the specfied header"""
|
||||||
|
return self.headers[hname]
|
||||||
|
|
||||||
|
def __getitem__(self, hname):
|
||||||
|
"""Return the specfied header"""
|
||||||
|
return self.headers[hname]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.headers['Event']
|
||||||
|
|
||||||
|
def get_action_id(self):
|
||||||
|
return self.headers.get('ActionID',0000)
|
||||||
|
|
||||||
|
class Manager(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._sock = None # our socket
|
||||||
|
self._connected = threading.Event()
|
||||||
|
self._running = threading.Event()
|
||||||
|
|
||||||
|
# our hostname
|
||||||
|
self.hostname = socket.gethostname()
|
||||||
|
|
||||||
|
# our queues
|
||||||
|
self._message_queue = Queue.Queue()
|
||||||
|
self._response_queue = Queue.Queue()
|
||||||
|
self._event_queue = Queue.Queue()
|
||||||
|
|
||||||
|
# callbacks for events
|
||||||
|
self._event_callbacks = {}
|
||||||
|
|
||||||
|
self._reswaiting = [] # who is waiting for a response
|
||||||
|
|
||||||
|
# sequence stuff
|
||||||
|
self._seqlock = threading.Lock()
|
||||||
|
self._seq = 0
|
||||||
|
|
||||||
|
# some threads
|
||||||
|
self.message_thread = threading.Thread(target=self.message_loop)
|
||||||
|
self.event_dispatch_thread = threading.Thread(target=self.event_dispatch)
|
||||||
|
|
||||||
|
self.message_thread.setDaemon(True)
|
||||||
|
self.event_dispatch_thread.setDaemon(True)
|
||||||
|
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
"""
|
||||||
|
Check if we are connected or not.
|
||||||
|
"""
|
||||||
|
return self._connected.isSet()
|
||||||
|
|
||||||
|
def next_seq(self):
|
||||||
|
"""Return the next number in the sequence, this is used for ActionID"""
|
||||||
|
self._seqlock.acquire()
|
||||||
|
try:
|
||||||
|
return self._seq
|
||||||
|
finally:
|
||||||
|
self._seq += 1
|
||||||
|
self._seqlock.release()
|
||||||
|
|
||||||
|
def send_action(self, cdict={}, **kwargs):
|
||||||
|
"""
|
||||||
|
Send a command to the manager
|
||||||
|
|
||||||
|
If a list is passed to the cdict argument, each item in the list will
|
||||||
|
be sent to asterisk under the same header in the following manner:
|
||||||
|
|
||||||
|
cdict = {"Action": "Originate",
|
||||||
|
"Variable": ["var1=value", "var2=value"]}
|
||||||
|
send_action(cdict)
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
Action: Originate
|
||||||
|
Variable: var1=value
|
||||||
|
Variable: var2=value
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._connected.isSet():
|
||||||
|
raise ManagerException("Not connected")
|
||||||
|
|
||||||
|
# fill in our args
|
||||||
|
cdict.update(kwargs)
|
||||||
|
|
||||||
|
# set the action id
|
||||||
|
if not cdict.has_key('ActionID'): cdict['ActionID'] = '%s-%08x' % (self.hostname, self.next_seq())
|
||||||
|
clist = []
|
||||||
|
|
||||||
|
# generate the command
|
||||||
|
for key, value in cdict.items():
|
||||||
|
if isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
item = tuple([key, item])
|
||||||
|
clist.append('%s: %s' % item)
|
||||||
|
else:
|
||||||
|
item = tuple([key, value])
|
||||||
|
clist.append('%s: %s' % item)
|
||||||
|
clist.append(EOL)
|
||||||
|
command = EOL.join(clist)
|
||||||
|
|
||||||
|
# lock the soket and send our command
|
||||||
|
try:
|
||||||
|
self._sock.sendall(command)
|
||||||
|
except socket.error, (errno, reason):
|
||||||
|
raise ManagerSocketException(errno, reason)
|
||||||
|
|
||||||
|
self._reswaiting.insert(0,1)
|
||||||
|
response = self._response_queue.get()
|
||||||
|
self._reswaiting.pop(0)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
raise ManagerSocketException(0, 'Connection Terminated')
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _receive_data(self):
|
||||||
|
"""
|
||||||
|
Read the response from a command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# loop while we are sill running and connected
|
||||||
|
while self._running.isSet() and self._connected.isSet():
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
# if there is data to be read
|
||||||
|
# read a message
|
||||||
|
while self._connected.isSet():
|
||||||
|
line = []
|
||||||
|
|
||||||
|
# read a line, one char at a time
|
||||||
|
while self._connected.isSet():
|
||||||
|
c = self._sock.recv(1)
|
||||||
|
|
||||||
|
if not c: # the other end closed the connection
|
||||||
|
self._sock.close()
|
||||||
|
self._connected.clear()
|
||||||
|
break
|
||||||
|
|
||||||
|
line.append(c) # append the character to our line
|
||||||
|
|
||||||
|
# is this the end of a line?
|
||||||
|
if c == '\n':
|
||||||
|
line = ''.join(line)
|
||||||
|
break
|
||||||
|
|
||||||
|
# if we are no longer connected we probably did not
|
||||||
|
# recieve a full message, don't try to handle it
|
||||||
|
if not self._connected.isSet(): break
|
||||||
|
|
||||||
|
# make sure our line is a string
|
||||||
|
assert type(line) in StringTypes
|
||||||
|
|
||||||
|
lines.append(line) # add the line to our message
|
||||||
|
|
||||||
|
# if the line is our EOL marker we have a complete message
|
||||||
|
if line == EOL:
|
||||||
|
break
|
||||||
|
|
||||||
|
# check to see if this is the greeting line
|
||||||
|
if line.find('/') >= 0 and line.find(':') < 0:
|
||||||
|
self.title = line.split('/')[0].strip() # store the title of the manager we are connecting to
|
||||||
|
self.version = line.split('/')[1].strip() # store the version of the manager we are connecting to
|
||||||
|
break
|
||||||
|
|
||||||
|
#sleep(.001) # waste some time before reading another line
|
||||||
|
|
||||||
|
except socket.error:
|
||||||
|
self._sock.close()
|
||||||
|
self._connected.clear()
|
||||||
|
break
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# if we have a message append it to our queue
|
||||||
|
if lines and self._connected.isSet():
|
||||||
|
self._message_queue.put(StringIO(''.join(lines)))
|
||||||
|
else:
|
||||||
|
self._message_queue.put(None)
|
||||||
|
|
||||||
|
def register_event(self, event, function):
|
||||||
|
"""
|
||||||
|
Register a callback for the specfied event.
|
||||||
|
If a callback function returns True, no more callbacks for that
|
||||||
|
event will be executed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get the current value, or an empty list
|
||||||
|
# then add our new callback
|
||||||
|
current_callbacks = self._event_callbacks.get(event, [])
|
||||||
|
current_callbacks.append(function)
|
||||||
|
self._event_callbacks[event] = current_callbacks
|
||||||
|
|
||||||
|
def unregister_event(self, event, function):
|
||||||
|
"""
|
||||||
|
Unregister a callback for the specified event.
|
||||||
|
"""
|
||||||
|
current_callbacks = self._event_callbacks.get(event, [])
|
||||||
|
current_callbacks.remove(function)
|
||||||
|
self._event_callbacks[event] = current_callbacks
|
||||||
|
|
||||||
|
def message_loop(self):
|
||||||
|
"""
|
||||||
|
The method for the event thread.
|
||||||
|
This actually recieves all types of messages and places them
|
||||||
|
in the proper queues.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# start a thread to recieve data
|
||||||
|
t = threading.Thread(target=self._receive_data)
|
||||||
|
t.setDaemon(True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# loop getting messages from the queue
|
||||||
|
while self._running.isSet():
|
||||||
|
# get/wait for messages
|
||||||
|
data = self._message_queue.get()
|
||||||
|
|
||||||
|
# if we got None as our message we are done
|
||||||
|
if not data:
|
||||||
|
# notify the other queues
|
||||||
|
self._event_queue.put(None)
|
||||||
|
for waiter in self._reswaiting:
|
||||||
|
self._response_queue.put(None)
|
||||||
|
break
|
||||||
|
|
||||||
|
# parse the data
|
||||||
|
message = ManagerMsg(data)
|
||||||
|
|
||||||
|
# check if this is an event message
|
||||||
|
if message.has_header('Event'):
|
||||||
|
self._event_queue.put(Event(message))
|
||||||
|
# check if this is a response
|
||||||
|
elif message.has_header('Response'):
|
||||||
|
self._response_queue.put(message)
|
||||||
|
# this is an unknown message
|
||||||
|
else:
|
||||||
|
print 'No clue what we got\n%s' % message.data
|
||||||
|
finally:
|
||||||
|
# wait for our data receiving thread to exit
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
|
||||||
|
def event_dispatch(self):
|
||||||
|
"""This thread is responsible fore dispatching events"""
|
||||||
|
|
||||||
|
# loop dispatching events
|
||||||
|
while self._running.isSet():
|
||||||
|
# get/wait for an event
|
||||||
|
ev = self._event_queue.get()
|
||||||
|
|
||||||
|
# if we got None as an event, we are finished
|
||||||
|
if not ev:
|
||||||
|
break
|
||||||
|
|
||||||
|
# dispatch our events
|
||||||
|
|
||||||
|
# first build a list of the functions to execute
|
||||||
|
callbacks = self._event_callbacks.get(ev.name, [])
|
||||||
|
callbacks.extend(self._event_callbacks.get('*', []))
|
||||||
|
|
||||||
|
# now execute the functions
|
||||||
|
for callback in callbacks:
|
||||||
|
if callback(ev, self):
|
||||||
|
break
|
||||||
|
|
||||||
|
def connect(self, host, port=5038):
|
||||||
|
"""Connect to the manager interface"""
|
||||||
|
|
||||||
|
if self._connected.isSet():
|
||||||
|
raise ManagerException('Already connected to manager')
|
||||||
|
|
||||||
|
# make sure host is a string
|
||||||
|
assert type(host) in StringTypes
|
||||||
|
|
||||||
|
port = int(port) # make sure port is an int
|
||||||
|
|
||||||
|
# create our socket and connect
|
||||||
|
try:
|
||||||
|
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self._sock.connect((host,port))
|
||||||
|
except socket.error, (errno, reason):
|
||||||
|
raise ManagerSocketException(errno, reason)
|
||||||
|
|
||||||
|
# we are connected and running
|
||||||
|
self._connected.set()
|
||||||
|
self._running.set()
|
||||||
|
|
||||||
|
# start the event thread
|
||||||
|
self.message_thread.start()
|
||||||
|
|
||||||
|
# start the event dispatching thread
|
||||||
|
self.event_dispatch_thread.start()
|
||||||
|
|
||||||
|
# get our initial connection response
|
||||||
|
return self._response_queue.get()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Shutdown the connection to the manager"""
|
||||||
|
|
||||||
|
# if we are still running, logout
|
||||||
|
if self._running.isSet() and self._connected.isSet():
|
||||||
|
self.logoff()
|
||||||
|
|
||||||
|
if self._running.isSet():
|
||||||
|
# put None in the message_queue to kill our threads
|
||||||
|
self._message_queue.put(None)
|
||||||
|
|
||||||
|
# wait for the event thread to exit
|
||||||
|
self.message_thread.join()
|
||||||
|
|
||||||
|
# make sure we do not join our self (when close is called from event handlers)
|
||||||
|
if threading.currentThread() != self.event_dispatch_thread:
|
||||||
|
# wait for the dispatch thread to exit
|
||||||
|
self.event_dispatch_thread.join()
|
||||||
|
|
||||||
|
self._running.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def login(self, username, secret):
|
||||||
|
"""Login to the manager, throws ManagerAuthException when login falis"""
|
||||||
|
|
||||||
|
cdict = {'Action':'Login'}
|
||||||
|
cdict['Username'] = username
|
||||||
|
cdict['Secret'] = secret
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
if response.get_header('Response') == 'Error':
|
||||||
|
raise ManagerAuthException(response.get_header('Message'))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
"""Send a ping action to the manager"""
|
||||||
|
cdict = {'Action':'Ping'}
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
"""Logoff from the manager"""
|
||||||
|
|
||||||
|
cdict = {'Action':'Logoff'}
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def hangup(self, channel):
|
||||||
|
"""Hanup the specfied channel"""
|
||||||
|
|
||||||
|
cdict = {'Action':'Hangup'}
|
||||||
|
cdict['Channel'] = channel
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def status(self, channel = ''):
|
||||||
|
"""Get a status message from asterisk"""
|
||||||
|
|
||||||
|
cdict = {'Action':'Status'}
|
||||||
|
cdict['Channel'] = channel
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def redirect(self, channel, exten, priority='1', extra_channel='', context=''):
|
||||||
|
"""Redirect a channel"""
|
||||||
|
|
||||||
|
cdict = {'Action':'Redirect'}
|
||||||
|
cdict['Channel'] = channel
|
||||||
|
cdict['Exten'] = exten
|
||||||
|
cdict['Priority'] = priority
|
||||||
|
if context: cdict['Context'] = context
|
||||||
|
if extra_channel: cdict['ExtraChannel'] = extra_channel
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def originate(self, channel, exten, context='', priority='', timeout='', caller_id='', async=False, account='', variables={}):
|
||||||
|
"""Originate a call"""
|
||||||
|
|
||||||
|
cdict = {'Action':'Originate'}
|
||||||
|
cdict['Channel'] = channel
|
||||||
|
cdict['Exten'] = exten
|
||||||
|
if context: cdict['Context'] = context
|
||||||
|
if priority: cdict['Priority'] = priority
|
||||||
|
if timeout: cdict['Timeout'] = timeout
|
||||||
|
if caller_id: cdict['CallerID'] = caller_id
|
||||||
|
if async: cdict['Async'] = 'yes'
|
||||||
|
if account: cdict['Account'] = account
|
||||||
|
# join dict of vairables together in a string in the form of 'key=val|key=val'
|
||||||
|
# with the latest CVS HEAD this is no longer necessary
|
||||||
|
# if variables: cdict['Variable'] = '|'.join(['='.join((str(key), str(value))) for key, value in variables.items()])
|
||||||
|
if variables: cdict['Variable'] = ['='.join((str(key), str(value))) for key, value in variables.items()]
|
||||||
|
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def mailbox_status(self, mailbox):
|
||||||
|
"""Get the status of the specfied mailbox"""
|
||||||
|
|
||||||
|
cdict = {'Action':'MailboxStatus'}
|
||||||
|
cdict['Mailbox'] = mailbox
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def command(self, command):
|
||||||
|
"""Execute a command"""
|
||||||
|
|
||||||
|
cdict = {'Action':'Command'}
|
||||||
|
cdict['Command'] = command
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def extension_state(self, exten, context):
|
||||||
|
"""Get the state of an extension"""
|
||||||
|
|
||||||
|
cdict = {'Action':'ExtensionState'}
|
||||||
|
cdict['Exten'] = exten
|
||||||
|
cdict['Context'] = context
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def absolute_timeout(self, channel, timeout):
|
||||||
|
"""Set an absolute timeout on a channel"""
|
||||||
|
|
||||||
|
cdict = {'Action':'AbsoluteTimeout'}
|
||||||
|
cdict['Channel'] = channel
|
||||||
|
cdict['Timeout'] = timeout
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def mailbox_count(self, mailbox):
|
||||||
|
cdict = {'Action':'MailboxCount'}
|
||||||
|
cdict['Mailbox'] = mailbox
|
||||||
|
response = self.send_action(cdict)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
class ManagerException(Exception): pass
|
||||||
|
class ManagerSocketException(ManagerException): pass
|
||||||
|
class ManagerAuthException(ManagerException): pass
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
pyst (0.2-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* New release.
|
||||||
|
* Package updated to reflect new python policy.
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Tue, 19 Sep 2006 19:22:51 -0500
|
||||||
|
|
||||||
|
pyst (0.1.0-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* New release.
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Mon, 5 Jun 2006 15:11:24 -0500
|
||||||
|
|
||||||
|
pyst (0.0.4rc13-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* New release.
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Tue, 21 Mar 2006 12:25:03 -0600
|
||||||
|
|
||||||
|
pyst (0.0.4rc12-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* New release.
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Thu, 14 Apr 2005 17:15:46 -0500
|
||||||
|
|
||||||
|
pyst (0.0.4rc11-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* New release.
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Thu, 3 Feb 2005 18:46:32 -0600
|
||||||
|
|
||||||
|
pyst (0.0.4rc10-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* Bug fixes in config.py and more docs.
|
||||||
|
* Version number correction.
|
||||||
|
* Added line number tracking to config.py
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Tue, 11 Jan 2005 11:30:59 -0600
|
||||||
|
|
||||||
|
pyst (0.0.4rc9-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* Removed DEBUG flag
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Mon, 10 Jan 2005 18:40:04 -0600
|
||||||
|
|
||||||
|
pyst (0.0.4rc8-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* More manager.py updates
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Thu, 6 Jan 2005 12:19:42 -0600
|
||||||
|
|
||||||
|
pyst (0.0.4rc7-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* A bunch of fixes to make the manager interface useable
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Wed, 5 Jan 2005 19:02:38 -0600
|
||||||
|
|
||||||
|
pyst (0.0.4rc6-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* Initial Release.
|
||||||
|
|
||||||
|
-- Matthew Nicholson <mnicholson@digium.com> Mon, 29 Nov 2004 14:06:42 -0600
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
4
|
|
@ -0,0 +1,21 @@
|
||||||
|
Source: pyst
|
||||||
|
Section: python
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Matthew Nicholson <mnicholson@digium.com>
|
||||||
|
Build-Depends-Indep: debhelper (>= 5.0.37.2), cdbs (>= 0.4.43), python-all-dev (>= 2.3.5-11), python-support (>= 0.3)
|
||||||
|
Standards-Version: 3.6.1
|
||||||
|
|
||||||
|
Package: python-pyst
|
||||||
|
Architecture: all
|
||||||
|
Depends: ${python:Depends}
|
||||||
|
Provides: ${python:Provides}
|
||||||
|
Replaces: python2.3-pyst (<< 0.2)
|
||||||
|
Conflicts: python2.3-pyst (<< 0.2)
|
||||||
|
XB-Python-Version: ${python:Versions}
|
||||||
|
Suggests: asterisk
|
||||||
|
Description: Python module for interacting with the Asterisk PBX (dummy)
|
||||||
|
Pyst is a python module for interacting with the asterisk pbx through
|
||||||
|
the manager interface and agi interface. This package also includes
|
||||||
|
debugging tools such as the agitb module, and tools to parse asterisk
|
||||||
|
config files.
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
This package was debianized by Matthew Nicholson <mnicholson@digium.com> on
|
||||||
|
Mon, 29 Nov 2004 14:06:42 -0600.
|
||||||
|
|
||||||
|
It was downloaded from http://ftp1.sourceforge.net/pyst/pyst-0.0.4.tar.gz
|
||||||
|
|
||||||
|
Copyright (C) 2004 Karl Putland
|
||||||
|
|
||||||
|
Upstream Author: Karl Putland <kputland@users.sourceforge.net>
|
||||||
|
|
||||||
|
License:
|
||||||
|
|
||||||
|
Most of pyst is licensed under the terms of the GNU LGPL.
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
The agitb.py modules is licensed under the terms of the Python Software
|
||||||
|
Foundation License:
|
||||||
|
|
||||||
|
PSF LICENSE AGREEMENT FOR PYTHON 2.3
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||||
|
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||||
|
otherwise using Python 2.3 software in source or binary form and its
|
||||||
|
associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, PSF
|
||||||
|
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||||
|
license to reproduce, analyze, test, perform and/or display publicly,
|
||||||
|
prepare derivative works, distribute, and otherwise use Python 2.3
|
||||||
|
alone or in any derivative version, provided, however, that PSF's
|
||||||
|
License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
|
||||||
|
2001, 2002 Python Software Foundation; All Rights Reserved" are
|
||||||
|
retained in Python 2.3 alone or in any derivative version prepared by
|
||||||
|
Licensee.
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python 2.3 or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python 2.3.
|
||||||
|
|
||||||
|
4. PSF is making Python 2.3 available to Licensee on an "AS IS"
|
||||||
|
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.3 WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
2.3 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.3,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. Nothing in this License Agreement shall be deemed to create any
|
||||||
|
relationship of agency, partnership, or joint venture between PSF and
|
||||||
|
Licensee. This License Agreement does not grant permission to use PSF
|
||||||
|
trademarks or trade name in a trademark sense to endorse or promote
|
||||||
|
products or services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By copying, installing or otherwise using Python 2.3, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
# Uncomment this to turn on verbose mode.
|
||||||
|
#export DH_VERBOSE=1
|
||||||
|
|
||||||
|
DEB_PYTHON_SYSTEM=pysupport
|
||||||
|
|
||||||
|
include /usr/share/cdbs/1/rules/debhelper.mk
|
||||||
|
include /usr/share/cdbs/1/class/python-distutils.mk
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Site Directory Pattern Version Script
|
||||||
|
version=2
|
||||||
|
http://ftp1.sourceforge.net/pyst/pyst-(.*)\.tar\.gz debian uupdate
|
|
@ -0,0 +1,48 @@
|
||||||
|
Summary: An interface to AGI
|
||||||
|
Name: python-pyst
|
||||||
|
Version: 0.0.5
|
||||||
|
Release: 2.centos4.0
|
||||||
|
Source0: http://prdownloads.sourceforge.net/pyst/pyst-%{version}.tar.gz
|
||||||
|
License: LGPL
|
||||||
|
Group: Development/Libraries
|
||||||
|
BuildRoot: %{_tmppath}/%{name}-buildroot
|
||||||
|
URL: http://sourceforge.net/projects/pyst
|
||||||
|
Requires: python
|
||||||
|
BuildRequires: python-devel
|
||||||
|
BuildRequires: python Distutils
|
||||||
|
|
||||||
|
%description
|
||||||
|
Pyst consists of a set of interfaces and libraries to allow
|
||||||
|
programming of Asterisk from python. The library currently
|
||||||
|
supports AGI, AMI, and the parsing of Asterisk configuration
|
||||||
|
files. The library also includes debugging facilities for AGI.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup -q -n pyst-%{version}
|
||||||
|
|
||||||
|
%build
|
||||||
|
CFLAGS="$RPM_OPT_FLAGS" python setup.py build
|
||||||
|
|
||||||
|
%install
|
||||||
|
python setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/usr/share/doc/python-pyst
|
||||||
|
cp debian/copyright $RPM_BUILD_ROOT/usr/share/doc/python-pyst
|
||||||
|
|
||||||
|
%clean
|
||||||
|
rm -rf $RPM_BUILD_ROOT
|
||||||
|
|
||||||
|
%files -f INSTALLED_FILES
|
||||||
|
%defattr(-,root,root)
|
||||||
|
%doc /usr/share/doc/python-pyst/*
|
||||||
|
%{_libdir}/python*/site-packages/asterisk/
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Tue Mar 21 2006 Matthew Nicholson <mnicholson@digium.com> el4.3
|
||||||
|
- Bumped version number.
|
||||||
|
|
||||||
|
* Thu Feb 23 2006 Antoine Brenner <http://www.gymglish.com> el4.2
|
||||||
|
- Fixed source0 line
|
||||||
|
|
||||||
|
* Tue Feb 9 2006 Antoine Brenner <http://www.gymglish.com> el4.1
|
||||||
|
- Initial Package
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
setup(name = 'pyst',
|
||||||
|
version = '0.2',
|
||||||
|
description = 'Asterisk related utility modules.',
|
||||||
|
author = 'Karl Putland',
|
||||||
|
author_email = 'kputland@users.sourceforge.net',
|
||||||
|
url = 'http://www.sourceforge.net/projects/pyst/',
|
||||||
|
packages = ['asterisk'],
|
||||||
|
license = 'Python Software Foundation License (agitb), Lesser General Public License',
|
||||||
|
platforms = 'UNIX',
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue