├── .gitignore ├── CHANGELOG ├── MANIFEST.in ├── Makefile ├── README.rst ├── assets └── snake-sketch.jpg ├── asterisk ├── __init__.py ├── agi.py ├── agitb.py ├── config.py ├── fastagi.py └── manager.py ├── docs ├── Makefile └── source │ ├── agi.rst │ ├── agitb.rst │ ├── changes.rst │ ├── conf.py │ ├── config.rst │ ├── fastagi.rst │ ├── index.rst │ ├── manager.rst │ └── readme.rst ├── doctrees ├── agi.doctree ├── agitb.doctree ├── changes.doctree ├── config.doctree ├── environment.pickle ├── fastagi.doctree ├── index.doctree ├── manager.doctree └── readme.doctree ├── examples ├── agi_script.py └── show_channels.py ├── setup.py └── tests ├── __init__.py └── test_basic.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary files. 2 | *.py[cod] 3 | *.sw? 4 | *~ 5 | .coverage 6 | 7 | # Build remnants. 8 | html 9 | dist 10 | build 11 | _build 12 | *.egg-info 13 | 14 | # Python-related stuff 15 | env/ 16 | .env/ 17 | venv/ 18 | __pycache__/ 19 | 20 | # Auto-generated files. 21 | MANIFEST 22 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2019-08-08 Francesco Rana 2 | 3 | * Fixed send_command to work with python3 (by evilscientress ) 4 | * changed 'async' option name in manager.py due to the updated reserved words list in Python 3 5 | 6 | 2016-11-04 VoiPCS 7 | 8 | * Fixing unicode bug in `send_action` for AMI. 9 | 10 | 2016-02-16 Randall Degges 11 | 12 | * Fixing issue in AGI class init function. Thanks to @sancho2934489 for the 13 | find! 14 | 15 | 2015-11-16 Scinawa Antani 16 | 17 | * Fixing indentation errors. 18 | 19 | 2015-11-14 Ben Davis 20 | 21 | * Handling more UTF-8 encoding issues in `_quote` method. 22 | 23 | 2015-07-18 Artem Sorokin 24 | 25 | * Fixing UTF-8 encoding issues. 26 | 27 | 2015-07-15 Artem Sorokin 28 | 29 | * Fix multiline command end marker for OpenVox GSM Gateway. 30 | 31 | 2015-03-31 Randall Degges 32 | 33 | * Porting packaging to setuptools (modern). 34 | * Adding six as a dependency (it was missing before). 35 | 36 | 2015-03-30 Areski Belaid 37 | 38 | * Fixing the MANIFEST.in file I accidentally broke :) 39 | 40 | 2015-03-29 Timur Tuchkovenko 41 | * UPGRADE: AMI fix for Python 3 compatibility. 42 | 43 | 2014-10-08 Timur Tuchkovenko 44 | * UPGRADE: initial Python 3 support. Now pyst2 requires 45 | Python 'six' module. Some minor changes in other files. 46 | 47 | 2014-09-14 Sp1tF1r3 48 | * asterisk/manager.py: added action 'Reload' for Asterisk Manager 49 | Interface (AMI). 50 | 51 | 2013-12-03 Ludovic Gasc 52 | * examples/agi_script.py: added example script to explain AGI 53 | functionality. 54 | * README: renamed to REAMDE.rst for Github's Markdown support. 55 | * setup.py: minor changes. 56 | 57 | 2012-11-12 Arezqui Belaid 58 | * asterisk/manager.py: minor empty line enhancements. 59 | * examples/show_channels.py: added example script to show information via 60 | Asterisk Manager Interface (AMI). 61 | 62 | 2012-11-11 Arezqui Belaid 63 | * PEP8 Fixes 64 | 65 | 2011-05-31 Randall Degges 66 | * BUGFIX: Fixing issue that prevented manager.status command from returning 67 | proper output. 68 | 69 | 2007-01-26 Matthew Nicholson 70 | 71 | * asterisk/manager.py: Make get_header() functions work like 72 | dict.get(). 73 | * UPGRADE: Updated. 74 | 75 | 2007-01-16 Matthew Nicholson 76 | 77 | * asterisk/manager.py: Fix support for Manager.command(). Patch from 78 | Karl Putland . 79 | 80 | 2007-01-02 Matthew Nicholson 81 | 82 | * asterisk/agi.py (AGI.set_autohangup): Fixed syntax error. 83 | 84 | 2006-11-28 Matthew Nicholson 85 | 86 | * UPGRADE: Tweaked formatting. 87 | 88 | 2006-10-30 Matthew Nicholson 89 | 90 | * ChangeLog: Fixed previous entry. 91 | 92 | 2006-10-30 Matthew Nicholson 93 | 94 | * TODO: Updated. 95 | * asterisk/agi.py (AGI.control_stream_file): Changed default skipms 96 | and quoted arguments. 97 | 98 | 2006-10-24 Matthew Nicholson 99 | 100 | * asterisk/agi.py: Added get_variable_full command. 101 | 102 | 2006-10-18 Matthew Nicholson 103 | 104 | * asterisk/agitb.py: Make error output default to sys.stderr instead 105 | of sys.stdout. 106 | 107 | 2006-09-19 Matthew Nicholson 108 | 109 | * debian/control: Removed XS-Python-Versions header to make it default 110 | to all python versions. 111 | 112 | 2006-09-19 Matthew Nicholson 113 | 114 | * setup.py: Updated version. 115 | 116 | 2006-09-19 Matthew Nicholson 117 | 118 | * debian/rules: Changed to use pysupport. 119 | * debian/control: Changed to use pysupport and changed arch to all. 120 | 121 | 2006-09-19 Matthew Nicholson 122 | 123 | * MANIFEST.in: Added NEWS to manifest. 124 | 125 | 2006-09-19 Matthew Nicholson 126 | 127 | * debian/rules: Updated to reflect new python policy. 128 | * debian/control: Updated to reflect new python policy. 129 | * debian/changelog: Updated. 130 | 131 | 2006-08-23 Matthew Nicholson 132 | 133 | * UPGRADE: Updated. 134 | 135 | 2006-08-23 Matthew Nicholson 136 | 137 | * asterisk/manager.py (unregister_event): Added. 138 | 139 | 2006-08-23 Matthew Nicholson 140 | 141 | * NEWS: Added. 142 | 143 | 2006-07-14 Matthew Nicholson 144 | 145 | * asterisk/agi.py (wait_for_digit): Only catch ValueError, not all 146 | exceptions. 147 | 148 | 2006-07-14 Matthew Nicholson 149 | 150 | * TODO: Updated. 151 | * asterisk/agi.py (set_variable): Documentation changes. 152 | * asterisk/agi.py (get_variable): Changed to return and empty string 153 | instead of throwing an exception when a channel variable is not set. 154 | * UPGRADE: Added. 155 | 156 | 2006-07-14 Matthew Nicholson 157 | 158 | * ChangeLog: Added. 159 | * TODO: Added. 160 | * MANIFEST.in: Added ChangeLog and TODO. 161 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG 2 | include README.rst 3 | include MANIFEST.in 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CDUP=../.. 2 | PKG=asterisk 3 | PY=agi.py agitb.py config.py __init__.py manager.py 4 | SRC=Makefile MANIFEST.in setup.py README README.html \ 5 | $(PY:%.py=$(PKG)/%.py) 6 | 7 | VERSIONPY=asterisk/Version.py 8 | VERSION=$(VERSIONPY) 9 | LASTRELEASE:=$(shell ../svntools/lastrelease -n) 10 | 11 | USERNAME=schlatterbeck 12 | PROJECT=pyst2 13 | PACKAGE=${PROJECT} 14 | CHANGES=changes 15 | NOTES=notes 16 | 17 | all: $(VERSION) 18 | 19 | $(VERSION): $(SRC) 20 | 21 | dist: all 22 | python setup.py sdist --formats=gztar,zip 23 | 24 | clean: 25 | rm -f MANIFEST README.html default.css \ 26 | $(PKG)/Version.py $(PKG)/Version.pyc ${CHANGES} ${NOTES} \ 27 | upload ReleaseNotes.txt announce_pypi upload_homepage 28 | rm -rf dist build 29 | 30 | 31 | release: upload upload_homepage announce_pypi announce 32 | 33 | include ../make/Makefile-sf 34 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyst2: A Python Interface to Asterisk 2 | ===================================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/pyst2.svg 5 | :alt: pyst2 Release 6 | :target: https://pypi.python.org/pypi/pyst2 7 | 8 | .. image:: https://img.shields.io/pypi/dm/pyst2.svg 9 | :alt: pyst2 Downloads 10 | :target: https://pypi.python.org/pypi/pyst2 11 | 12 | .. image:: https://img.shields.io/travis/rdegges/pyst2.svg 13 | :alt: pyst2 Build 14 | :target: https://travis-ci.org/rdegges/pyst2 15 | 16 | .. image:: https://github.com/rdegges/pyst2/raw/master/assets/snake-sketch.jpg 17 | :alt: Snake Sketch 18 | 19 | Project Documentation 20 | --------------------- 21 | 22 | http://pyst2.readthedocs.io 23 | 24 | 25 | Meta 26 | ---- 27 | 28 | - Author: Randall Degges 29 | - Email: r@rdegges.com 30 | - Site: http://www.rdegges.com 31 | - Status: *looking for maintainer*, active 32 | 33 | **NOTE**: This project is now mantained by Francesco Rana. 34 | Please be patient because I'm not used to the job yet, but I'll do my best. 35 | Many and infinite thanks to Randall Degges for his wonderful work. I'm actually using the 36 | library in some project of mine, so I'm more than happy to help and push it further if I can. 37 | I'm happy to accept pull requests and cut releases as needed. 38 | If you want to contribute to the project, please do! 39 | 40 | 41 | Purpose 42 | ------- 43 | 44 | pyst2 consists of a set of interfaces and libraries to allow programming of 45 | Asterisk from python. The library currently supports AGI, AMI, and the parsing 46 | of Asterisk configuration files. The library also includes debugging facilities 47 | for AGI. 48 | 49 | This project has been forked from pyst (http://sf.net/projects/pyst/) because 50 | it was impossible for me to contact the project maintainer (after several 51 | attempts), and I'd like to bring the project up-to-date, fix bugs, and make 52 | it more usable overall. 53 | 54 | My immediate plans include adding full documentation, re-writing some 55 | of the core routines, adding a test suite, and accepting pull requests. 56 | 57 | If you are one of the current maintainers, and would like to take over the 58 | fork, please contact me: r@rdegges.com, so we can get that setup! 59 | 60 | 61 | Installation 62 | ------------ 63 | 64 | To install ``pyst2``, simply run: 65 | 66 | .. code-block:: console 67 | 68 | $ pip install pyst2 69 | 70 | This will install the latest version of the library automatically. 71 | 72 | 73 | Documentation 74 | ------------- 75 | 76 | Documentation is currently only in python docstrings, you can use 77 | pythons built-in help facility:: 78 | 79 | import asterisk 80 | help (asterisk) 81 | import asterisk.agi 82 | help (asterisk.agi) 83 | import asterisk.manager 84 | help (asterisk.manager) 85 | import asterisk.config 86 | help (asterisk.config) 87 | 88 | Some notes on platforms: We now specify "platforms = 'Any'" in 89 | ``setup.py``. This means, the manager part of the package will probably 90 | run on any platform. The agi scripts on the other hand are called 91 | directly on the host where Asterisk is running. Since Asterisk doesn't 92 | run on windows platforms (and probably never will) the agi part of the 93 | package can only be run on Asterisk platforms. 94 | 95 | FastAGI 96 | ------- 97 | 98 | FastAGI support is a python based raw SocketServer, To start the server 99 | python fastagi.py should start it listening on localhost and the default 100 | asterisk FastAGI port. This does require the newest version of pyst2. 101 | The FastAGI server runs in as a Forked operation for each request, in 102 | an attempt to prevent blocking by a single bad service. As a result the 103 | FastAGI server may consume more memory then a single process. If you need 104 | to use a single process simply uncomment the appropriate line. Future versions 105 | of this will use a config file to set options. 106 | 107 | Credits 108 | ------- 109 | 110 | Thanks to Karl Putland for writing the original package. 111 | 112 | Thanks to Matthew Nicholson for maintaining the package for some years 113 | and for handing over maintenance when he was no longer interested. 114 | 115 | Thanks to Randall Degges for maintaining this for and accepting 116 | pull requests. 117 | 118 | 119 | Things to do for pyst 120 | --------------------- 121 | 122 | This is the original changelog merged into the readme file. I'm not so 123 | sure I really want to change all these things (in particular the 124 | threaded implementation looks good to me). I will maintain a section 125 | summarizing the changes in this README. Detailed changes will be 126 | available in the version control tool (currently git). 127 | 128 | * ChangeLog: 129 | The ChangeLog needs to be updated from the monotone logs. 130 | 131 | * Documentation: 132 | All of pyst's inline documentation needs to be updated. 133 | 134 | * manager.py: 135 | This should be converted to be single threaded. Also there is a race 136 | condition when a user calls manager.logoff() followed by 137 | manager.close(). The close() function may still call logoff again if 138 | the socket thread has not yet cleared the _connected flag. 139 | 140 | A class should be made for each manager action rather than having a 141 | function in a manager class. The manager class should be adapted to 142 | have a send method that know the general format of the classes. 143 | 144 | Matthew Nicholson writes on the mailinglist (note that I'm not sure I'll do 145 | this, I'm currently satisfied with the threaded implementation): 146 | 147 | For pyst 0.3 I am planning to clean up the manager.py. There are 148 | several know issues with the code. No one has actually reported these 149 | as problems, but I have personally had trouble with these. Currently 150 | manager.py runs in several threads, the main program thread, a thread to 151 | read from the network, and an event distribution thread. This causes 152 | problems with non thread safe code such as the MySQLdb libraries. This 153 | design also causes problems when an event handler throws an exception 154 | that causes the event processing thread to terminate. 155 | 156 | The second problem is with the way actions are sent. Each action has a 157 | specific function associated with it in the manager object that takes 158 | all possible arguments that may ever be passed to that action. This 159 | makes the api somewhat rigid and the Manager object cluttered. 160 | 161 | To solve these problems I am basically going to copy the design of my 162 | Astxx manager library (written in c++) and make it more python like. 163 | Each action will be a different object with certain methods to handle 164 | various tasks, with one function in the actual Manager class to send the 165 | action. This will make the Manager class much smaller and much more 166 | flexible. The current code will be consolidated into a single threaded 167 | design with hooks to have the library process events and such. These 168 | hooks will be called from the host application's main loop. 169 | -------------------------------------------------------------------------------- /assets/snake-sketch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/assets/snake-sketch.jpg -------------------------------------------------------------------------------- /asterisk/__init__.py: -------------------------------------------------------------------------------- 1 | """ pyst - A set of interfaces and libraries to allow programming of asterisk from python. 2 | 3 | The pyst project includes several python modules to assist in programming 4 | asterisk with python: 5 | 6 | agi - python wrapper for agi 7 | agitb - a module to assist in agi debugging, like cgitb 8 | config - a module for parsing asterisk config files 9 | manager - a module for interacting with the asterisk manager interface 10 | 11 | """ 12 | 13 | __all__ = ['agi', 'agitb', 'config', 'manager'] 14 | __version__ = '0.5.1' 15 | -------------------------------------------------------------------------------- /asterisk/agi.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim: set et sw=4 fenc=utf-8: 4 | """ 5 | .. module:: agi 6 | :synopsis: This module contains functions and classes to implment AGI scripts in python. 7 | 8 | pyvr 9 | 10 | {'agi_callerid' : 'mars.putland.int', 11 | 'agi_channel' : 'IAX[kputland@kputland]/119', 12 | 'agi_context' : 'default', 13 | 'agi_dnid' : '1000', 14 | 'agi_enhanced' : '0.0', 15 | 'agi_extension': '1000', 16 | 'agi_language' : 'en', 17 | 'agi_priority' : '1', 18 | 'agi_rdnis' : '', 19 | 'agi_request' : 'pyst', 20 | 'agi_type' : 'IAX'} 21 | 22 | Specification 23 | ------------- 24 | """ 25 | 26 | import sys 27 | import pprint 28 | import re 29 | import signal 30 | from six import PY3 31 | 32 | DEFAULT_TIMEOUT = 2000 # 2sec timeout used as default for functions that take timeouts 33 | DEFAULT_RECORD = 20000 # 20sec record time 34 | 35 | re_code = re.compile(r'(^\d*)\s*(.*)') 36 | re_kv = re.compile(r'(?P\w+)=(?P[^\s]+)\s*(?:\((?P.*)\))*') 37 | 38 | 39 | class AGIException(Exception): 40 | pass 41 | 42 | 43 | class AGIError(AGIException): 44 | pass 45 | 46 | 47 | class AGIUnknownError(AGIError): 48 | pass 49 | 50 | 51 | class AGIAppError(AGIError): 52 | pass 53 | 54 | # there are several different types of hangups we can detect 55 | # they all are derrived from AGIHangup 56 | 57 | 58 | class AGIHangup(AGIAppError): 59 | pass 60 | 61 | 62 | class AGISIGHUPHangup(AGIHangup): 63 | pass 64 | 65 | 66 | class AGISIGPIPEHangup(AGIHangup): 67 | pass 68 | 69 | 70 | class AGIResultHangup(AGIHangup): 71 | pass 72 | 73 | 74 | class AGIDBError(AGIAppError): 75 | pass 76 | 77 | 78 | class AGIUsageError(AGIError): 79 | pass 80 | 81 | 82 | class AGIInvalidCommand(AGIError): 83 | pass 84 | 85 | 86 | class AGI: 87 | """ 88 | This class encapsulates communication between Asterisk an a python script. 89 | It handles encoding commands to Asterisk and parsing responses from 90 | Asterisk. 91 | """ 92 | 93 | def __init__(self, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr): 94 | self.stdin = stdin 95 | self.stdout = stdout 96 | self.stderr = stderr 97 | self._got_sighup = False 98 | signal.signal(signal.SIGHUP, self._handle_sighup) # handle SIGHUP 99 | self.stderr.write('ARGS: ') 100 | self.stderr.write(str(sys.argv)) 101 | self.stderr.write('\n') 102 | self.env = {} 103 | self._get_agi_env() 104 | 105 | def _get_agi_env(self): 106 | while 1: 107 | line = self.stdin.readline().strip() 108 | if PY3: 109 | if type(line) is bytes: line = line.decode('utf8') 110 | self.stderr.write('ENV LINE: ') 111 | self.stderr.write(line) 112 | self.stderr.write('\n') 113 | if line == '': 114 | #blank line signals end 115 | break 116 | key, data = line.split(':')[0], ':'.join(line.split(':')[1:]) 117 | key = key.strip() 118 | data = data.strip() 119 | if key != '': 120 | self.env[key] = data 121 | self.stderr.write('class AGI: self.env = ') 122 | self.stderr.write(pprint.pformat(self.env)) 123 | self.stderr.write('\n') 124 | 125 | def _quote(self, string): 126 | """ provides double quotes to string, converts int/bool to string """ 127 | if isinstance(string, int): 128 | string = str(string) 129 | if isinstance(string, float): 130 | string = str(string) 131 | if PY3: 132 | return ''.join(['"', string, '"']) 133 | else: 134 | return ''.join(['"', string.encode('utf8', 'ignore'), '"']) 135 | 136 | def _handle_sighup(self, signum, frame): 137 | """Handle the SIGHUP signal""" 138 | self._got_sighup = True 139 | 140 | def test_hangup(self): 141 | """This function throws AGIHangup if we have recieved a SIGHUP""" 142 | if self._got_sighup: 143 | raise AGISIGHUPHangup("Received SIGHUP from Asterisk") 144 | 145 | def execute(self, command, *args): 146 | self.test_hangup() 147 | 148 | try: 149 | self.send_command(command, *args) 150 | return self.get_result() 151 | except IOError as e: 152 | if e.errno == 32: 153 | # Broken Pipe * let us go 154 | raise AGISIGPIPEHangup("Received SIGPIPE") 155 | else: 156 | raise 157 | 158 | def send_command(self, command, *args): 159 | """Send a command to Asterisk""" 160 | command = command.strip() 161 | command = '%s %s' % (command, ' '.join(map(str, args))) 162 | command = command.strip() 163 | if command[-1] != '\n': 164 | command += '\n' 165 | self.stderr.write(' COMMAND: %s' % command) 166 | self.stdout.write(command) 167 | self.stdout.flush() 168 | 169 | def get_result(self, stdin=sys.stdin): 170 | """Read the result of a command from Asterisk""" 171 | code = 0 172 | result = {'result': ('', '')} 173 | line = self.stdin.readline().strip() 174 | if PY3: 175 | if type(line) is bytes: line = line.decode('utf8') 176 | self.stderr.write(' RESULT_LINE: %s\n' % line) 177 | m = re_code.search(line) 178 | if m: 179 | code, response = m.groups() 180 | code = int(code) 181 | 182 | if code == 200: 183 | for key, value, data in re_kv.findall(response): 184 | result[key] = (value, data) 185 | 186 | # If user hangs up... we get 'hangup' in the data 187 | if data == 'hangup': 188 | raise AGIResultHangup("User hungup during execution") 189 | 190 | if key == 'result' and value == '-1': 191 | raise AGIAppError("Error executing application, or hangup") 192 | 193 | self.stderr.write(' RESULT_DICT: %s\n' % pprint.pformat(result)) 194 | return result 195 | elif code == 510: 196 | raise AGIInvalidCommand(response) 197 | elif code == 520: 198 | usage = [line] 199 | line = self.stdin.readline().strip() 200 | if PY3: 201 | if type(line) is bytes: line = line.decode('utf8') 202 | while line[:3] != '520': 203 | usage.append(line) 204 | line = self.stdin.readline().strip() 205 | if PY3: 206 | if type(line) is bytes: line = line.decode('utf8') 207 | usage.append(line) 208 | usage = '%s\n' % '\n'.join(usage) 209 | raise AGIUsageError(usage) 210 | else: 211 | raise AGIUnknownError(code, 'Unhandled code or undefined response') 212 | 213 | def _process_digit_list(self, digits): 214 | if type(digits) is list: 215 | digits = ''.join(map(str, digits)) 216 | return self._quote(digits) 217 | 218 | def answer(self): 219 | """agi.answer() --> None 220 | Answer channel if not already in answer state. 221 | """ 222 | self.execute('ANSWER')['result'][0] 223 | 224 | def wait_for_digit(self, timeout=DEFAULT_TIMEOUT): 225 | """agi.wait_for_digit(timeout=DEFAULT_TIMEOUT) --> digit 226 | Waits for up to 'timeout' milliseconds for a channel to receive a DTMF 227 | digit. Returns digit dialed 228 | Throws AGIError on channel falure 229 | """ 230 | res = self.execute('WAIT FOR DIGIT', timeout)['result'][0] 231 | if res == '0': 232 | return '' 233 | else: 234 | try: 235 | return chr(int(res)) 236 | except ValueError: 237 | raise AGIError('Unable to convert result to digit: %s' % res) 238 | 239 | def send_text(self, text=''): 240 | """agi.send_text(text='') --> None 241 | Sends the given text on a channel. Most channels do not support the 242 | transmission of text. 243 | Throws AGIError on error/hangup 244 | """ 245 | self.execute('SEND TEXT', self._quote(text))['result'][0] 246 | 247 | def receive_char(self, timeout=DEFAULT_TIMEOUT): 248 | """agi.receive_char(timeout=DEFAULT_TIMEOUT) --> chr 249 | Receives a character of text on a channel. Specify timeout to be the 250 | maximum time to wait for input in milliseconds, or 0 for infinite. Most channels 251 | do not support the reception of text. 252 | """ 253 | res = self.execute('RECEIVE CHAR', timeout)['result'][0] 254 | 255 | if res == '0': 256 | return '' 257 | else: 258 | try: 259 | return chr(int(res)) 260 | except: 261 | raise AGIError('Unable to convert result to char: %s' % res) 262 | 263 | def tdd_mode(self, mode='off'): 264 | """agi.tdd_mode(mode='on'|'off') --> None 265 | Enable/Disable TDD transmission/reception on a channel. 266 | Throws AGIAppError if channel is not TDD-capable. 267 | """ 268 | res = self.execute('TDD MODE', mode)['result'][0] 269 | if res == '0': 270 | raise AGIAppError('Channel %s is not TDD-capable') 271 | 272 | def stream_file(self, filename, escape_digits='', sample_offset=0): 273 | """agi.stream_file(filename, escape_digits='', sample_offset=0) --> digit 274 | Send the given file, allowing playback to be interrupted by the given 275 | digits, if any. escape_digits is a string '12345' or a list of 276 | ints [1,2,3,4,5] or strings ['1','2','3'] or mixed [1,'2',3,'4'] 277 | If sample offset is provided then the audio will seek to sample 278 | offset before play starts. Returns digit if one was pressed. 279 | Throws AGIError if the channel was disconnected. Remember, the file 280 | extension must not be included in the filename. 281 | """ 282 | escape_digits = self._process_digit_list(escape_digits) 283 | response = self.execute( 284 | 'STREAM FILE', filename, escape_digits, sample_offset) 285 | res = response['result'][0] 286 | if res == '0': 287 | return '' 288 | else: 289 | try: 290 | return chr(int(res)) 291 | except: 292 | raise AGIError('Unable to convert result to char: %s' % res) 293 | 294 | def control_stream_file(self, filename, escape_digits='', skipms=3000, fwd='', rew='', pause=''): 295 | """ 296 | Send the given file, allowing playback to be interrupted by the given 297 | digits, if any. escape_digits is a string '12345' or a list of 298 | ints [1,2,3,4,5] or strings ['1','2','3'] or mixed [1,'2',3,'4'] 299 | If sample offset is provided then the audio will seek to sample 300 | offset before play starts. Returns digit if one was pressed. 301 | Throws AGIError if the channel was disconnected. Remember, the file 302 | extension must not be included in the filename. 303 | """ 304 | escape_digits = self._process_digit_list(escape_digits) 305 | response = self.execute('CONTROL STREAM FILE', self._quote(filename), escape_digits, self._quote(skipms), self._quote(fwd), self._quote(rew), self._quote(pause)) 306 | res = response['result'][0] 307 | if res == '0': 308 | return '' 309 | else: 310 | try: 311 | return chr(int(res)) 312 | except: 313 | raise AGIError('Unable to convert result to char: %s' % res) 314 | 315 | def send_image(self, filename): 316 | """agi.send_image(filename) --> None 317 | Sends the given image on a channel. Most channels do not support the 318 | transmission of images. Image names should not include extensions. 319 | Throws AGIError on channel failure 320 | """ 321 | res = self.execute('SEND IMAGE', filename)['result'][0] 322 | if res != '0': 323 | raise AGIAppError('Channel falure on channel %s' % 324 | self.env.get('agi_channel', 'UNKNOWN')) 325 | 326 | def say_digits(self, digits, escape_digits=''): 327 | """agi.say_digits(digits, escape_digits='') --> digit 328 | Say a given digit string, returning early if any of the given DTMF digits 329 | are received on the channel. 330 | Throws AGIError on channel failure 331 | """ 332 | digits = self._process_digit_list(digits) 333 | escape_digits = self._process_digit_list(escape_digits) 334 | res = self.execute('SAY DIGITS', digits, escape_digits)['result'][0] 335 | if res == '0': 336 | return '' 337 | else: 338 | try: 339 | return chr(int(res)) 340 | except: 341 | raise AGIError('Unable to convert result to char: %s' % res) 342 | 343 | def say_number(self, number, escape_digits=''): 344 | """agi.say_number(number, escape_digits='') --> digit 345 | Say a given digit string, returning early if any of the given DTMF digits 346 | are received on the channel. 347 | Throws AGIError on channel failure 348 | """ 349 | number = self._process_digit_list(number) 350 | escape_digits = self._process_digit_list(escape_digits) 351 | res = self.execute('SAY NUMBER', number, escape_digits)['result'][0] 352 | if res == '0': 353 | return '' 354 | else: 355 | try: 356 | return chr(int(res)) 357 | except: 358 | raise AGIError('Unable to convert result to char: %s' % res) 359 | 360 | def say_alpha(self, characters, escape_digits=''): 361 | """agi.say_alpha(string, escape_digits='') --> digit 362 | Say a given character string, returning early if any of the given DTMF 363 | digits are received on the channel. 364 | Throws AGIError on channel failure 365 | """ 366 | characters = self._process_digit_list(characters) 367 | escape_digits = self._process_digit_list(escape_digits) 368 | res = self.execute('SAY ALPHA', characters, escape_digits)['result'][0] 369 | if res == '0': 370 | return '' 371 | else: 372 | try: 373 | return chr(int(res)) 374 | except: 375 | raise AGIError('Unable to convert result to char: %s' % res) 376 | 377 | def say_phonetic(self, characters, escape_digits=''): 378 | """agi.say_phonetic(string, escape_digits='') --> digit 379 | Phonetically say a given character string, returning early if any of 380 | the given DTMF digits are received on the channel. 381 | Throws AGIError on channel failure 382 | """ 383 | characters = self._process_digit_list(characters) 384 | escape_digits = self._process_digit_list(escape_digits) 385 | res = self.execute( 386 | 'SAY PHONETIC', characters, escape_digits)['result'][0] 387 | if res == '0': 388 | return '' 389 | else: 390 | try: 391 | return chr(int(res)) 392 | except: 393 | raise AGIError('Unable to convert result to char: %s' % res) 394 | 395 | def say_date(self, seconds, escape_digits=''): 396 | """agi.say_date(seconds, escape_digits='') --> digit 397 | Say a given date, returning early if any of the given DTMF digits are 398 | pressed. The date should be in seconds since the UNIX Epoch (Jan 1, 1970 00:00:00) 399 | """ 400 | escape_digits = self._process_digit_list(escape_digits) 401 | res = self.execute('SAY DATE', seconds, escape_digits)['result'][0] 402 | if res == '0': 403 | return '' 404 | else: 405 | try: 406 | return chr(int(res)) 407 | except: 408 | raise AGIError('Unable to convert result to char: %s' % res) 409 | 410 | def say_time(self, seconds, escape_digits=''): 411 | """agi.say_time(seconds, escape_digits='') --> digit 412 | Say a given time, returning early if any of the given DTMF digits are 413 | pressed. The time should be in seconds since the UNIX Epoch (Jan 1, 1970 00:00:00) 414 | """ 415 | escape_digits = self._process_digit_list(escape_digits) 416 | res = self.execute('SAY TIME', seconds, escape_digits)['result'][0] 417 | if res == '0': 418 | return '' 419 | else: 420 | try: 421 | return chr(int(res)) 422 | except: 423 | raise AGIError('Unable to convert result to char: %s' % res) 424 | 425 | def say_datetime(self, seconds, escape_digits='', format='', zone=''): 426 | """agi.say_datetime(seconds, escape_digits='', format='', zone='') --> digit 427 | Say a given date in the format specified (see voicemail.conf), returning 428 | early if any of the given DTMF digits are pressed. The date should be 429 | in seconds since the UNIX Epoch (Jan 1, 1970 00:00:00). 430 | """ 431 | escape_digits = self._process_digit_list(escape_digits) 432 | if format: 433 | format = self._quote(format) 434 | res = self.execute( 435 | 'SAY DATETIME', seconds, escape_digits, format, zone)['result'][0] 436 | if res == '0': 437 | return '' 438 | else: 439 | try: 440 | return chr(int(res)) 441 | except: 442 | raise AGIError('Unable to convert result to char: %s' % res) 443 | 444 | def get_data(self, filename, timeout=DEFAULT_TIMEOUT, max_digits=255): 445 | """agi.get_data(filename, timeout=DEFAULT_TIMEOUT, max_digits=255) --> digits 446 | Stream the given file and receive dialed digits 447 | """ 448 | result = self.execute('GET DATA', filename, timeout, max_digits) 449 | res, value = result['result'] 450 | return res 451 | 452 | def get_option(self, filename, escape_digits='', timeout=0): 453 | """agi.get_option(filename, escape_digits='', timeout=0) --> digit 454 | Send the given file, allowing playback to be interrupted by the given 455 | digits, if any. escape_digits is a string '12345' or a list of 456 | ints [1,2,3,4,5] or strings ['1','2','3'] or mixed [1,'2',3,'4'] 457 | Returns digit if one was pressed. 458 | Throws AGIError if the channel was disconnected. Remember, the file 459 | extension must not be included in the filename. 460 | """ 461 | escape_digits = self._process_digit_list(escape_digits) 462 | if timeout: 463 | response = self.execute( 464 | 'GET OPTION', filename, escape_digits, timeout) 465 | else: 466 | response = self.execute('GET OPTION', filename, escape_digits) 467 | 468 | res = response['result'][0] 469 | if res == '0': 470 | return '' 471 | else: 472 | try: 473 | return chr(int(res)) 474 | except: 475 | raise AGIError('Unable to convert result to char: %s' % res) 476 | 477 | def set_context(self, context): 478 | """agi.set_context(context) 479 | Sets the context for continuation upon exiting the application. 480 | No error appears to be produced. Does not set exten or priority 481 | Use at your own risk. Ensure that you specify a valid context. 482 | """ 483 | self.execute('SET CONTEXT', context) 484 | 485 | def set_extension(self, extension): 486 | """agi.set_extension(extension) 487 | Sets the extension for continuation upon exiting the application. 488 | No error appears to be produced. Does not set context or priority 489 | Use at your own risk. Ensure that you specify a valid extension. 490 | """ 491 | self.execute('SET EXTENSION', extension) 492 | 493 | def set_priority(self, priority): 494 | """agi.set_priority(priority) 495 | Sets the priority for continuation upon exiting the application. 496 | No error appears to be produced. Does not set exten or context 497 | Use at your own risk. Ensure that you specify a valid priority. 498 | """ 499 | self.execute('set priority', priority) 500 | 501 | def goto_on_exit(self, context='', extension='', priority=''): 502 | context = context or self.env['agi_context'] 503 | extension = extension or self.env['agi_extension'] 504 | priority = priority or self.env['agi_priority'] 505 | self.set_context(context) 506 | self.set_extension(extension) 507 | self.set_priority(priority) 508 | 509 | def record_file(self, filename, format='gsm', escape_digits='#', timeout=DEFAULT_RECORD, offset=0, beep='beep', silence=0): 510 | """agi.record_file(filename, format, escape_digits, timeout=DEFAULT_TIMEOUT, offset=0, beep='beep', silence=0) --> None 511 | Record to a file until a given dtmf digit in the sequence is received. Returns 512 | '-1' on hangup or error. The format will specify what kind of file will be 513 | recorded. The is the maximum record time in milliseconds, or '-1' 514 | for no . is optional, and, if provided, will seek 515 | to the offset without exceeding the end of the file. is the number 516 | of seconds of silence allowed before the function returns despite the lack 517 | of dtmf digits or reaching . value must be preceded by 518 | 's=' and is also optional. 519 | """ 520 | escape_digits = self._process_digit_list(escape_digits) 521 | res = self.execute('RECORD FILE', self._quote(filename), format, 522 | escape_digits, timeout, offset, beep, ('s=%s' % silence))['result'][0] 523 | try: 524 | return chr(int(res)) 525 | except: 526 | raise AGIError('Unable to convert result to digit: %s' % res) 527 | 528 | def set_autohangup(self, secs): 529 | """agi.set_autohangup(secs) --> None 530 | Cause the channel to automatically hangup at seconds in the 531 | future. Of course it can be hungup before then as well. Setting to 532 | 0 will cause the autohangup feature to be disabled on this channel. 533 | """ 534 | self.execute('SET AUTOHANGUP', secs) 535 | 536 | def hangup(self, channel=''): 537 | """agi.hangup(channel='') 538 | Hangs up the specified channel. 539 | If no channel name is given, hangs up the current channel 540 | """ 541 | self.execute('HANGUP', channel) 542 | 543 | def appexec(self, application, options=''): 544 | """agi.appexec(application, options='') 545 | Executes with given . 546 | Returns whatever the application returns, or -2 on failure to find 547 | application 548 | """ 549 | result = self.execute('EXEC', application, self._quote(options)) 550 | res = result['result'][0] 551 | if res == '-2': 552 | raise AGIAppError('Unable to find application: %s' % application) 553 | return res 554 | 555 | def set_callerid(self, number): 556 | """agi.set_callerid(number) --> None 557 | Changes the callerid of the current channel. 558 | """ 559 | self.execute('SET CALLERID', number) 560 | 561 | def channel_status(self, channel=''): 562 | """agi.channel_status(channel='') --> int 563 | Returns the status of the specified channel. If no channel name is 564 | given the returns the status of the current channel. 565 | 566 | Return values: 567 | 0 Channel is down and available 568 | 1 Channel is down, but reserved 569 | 2 Channel is off hook 570 | 3 Digits (or equivalent) have been dialed 571 | 4 Line is ringing 572 | 5 Remote end is ringing 573 | 6 Line is up 574 | 7 Line is busy 575 | """ 576 | try: 577 | result = self.execute('CHANNEL STATUS', channel) 578 | except AGIHangup: 579 | raise 580 | except AGIAppError: 581 | result = {'result': ('-1', '')} 582 | 583 | return int(result['result'][0]) 584 | 585 | def set_variable(self, name, value): 586 | """Set a channel variable. 587 | """ 588 | self.execute('SET VARIABLE', self._quote(name), self._quote(value)) 589 | 590 | def get_variable(self, name): 591 | """Get a channel variable. 592 | 593 | This function returns the value of the indicated channel variable. If 594 | the variable is not set, an empty string is returned. 595 | """ 596 | try: 597 | result = self.execute('GET VARIABLE', self._quote(name)) 598 | except AGIResultHangup: 599 | result = {'result': ('1', 'hangup')} 600 | 601 | res, value = result['result'] 602 | return value 603 | 604 | def get_full_variable(self, name, channel=None): 605 | """Get a channel variable. 606 | 607 | This function returns the value of the indicated channel variable. If 608 | the variable is not set, an empty string is returned. 609 | """ 610 | try: 611 | if channel: 612 | result = self.execute('GET FULL VARIABLE', 613 | self._quote(name), self._quote(channel)) 614 | else: 615 | result = self.execute('GET FULL VARIABLE', self._quote(name)) 616 | 617 | except AGIResultHangup: 618 | result = {'result': ('1', 'hangup')} 619 | 620 | res, value = result['result'] 621 | return value 622 | 623 | def verbose(self, message, level=1): 624 | """agi.verbose(message='', level=1) --> None 625 | Sends to the console via verbose message system. 626 | is the the verbose level (1-4) 627 | """ 628 | self.execute('VERBOSE', self._quote(message), level) 629 | 630 | def database_get(self, family, key): 631 | """agi.database_get(family, key) --> str 632 | Retrieves an entry in the Asterisk database for a given family and key. 633 | Returns 0 if is not set. Returns 1 if 634 | is set and returns the variable in parenthesis 635 | example return code: 200 result=1 (testvariable) 636 | """ 637 | family = '"%s"' % family 638 | key = '"%s"' % key 639 | result = self.execute( 640 | 'DATABASE GET', self._quote(family), self._quote(key)) 641 | res, value = result['result'] 642 | if res == '0': 643 | raise AGIDBError('Key not found in database: family=%s, key=%s' % 644 | (family, key)) 645 | elif res == '1': 646 | return value 647 | else: 648 | raise AGIError('Unknown exception for : family=%s, key=%s, result=%s' % (family, key, pprint.pformat(result))) 649 | 650 | def database_put(self, family, key, value): 651 | """agi.database_put(family, key, value) --> None 652 | Adds or updates an entry in the Asterisk database for a 653 | given family, key, and value. 654 | """ 655 | result = self.execute('DATABASE PUT', self._quote( 656 | family), self._quote(key), self._quote(value)) 657 | res, value = result['result'] 658 | if res == '0': 659 | raise AGIDBError('Unable to put vaule in databale: family=%s, key=%s, value=%s' % (family, key, value)) 660 | 661 | def database_del(self, family, key): 662 | """agi.database_del(family, key) --> None 663 | Deletes an entry in the Asterisk database for a 664 | given family and key. 665 | """ 666 | result = self.execute( 667 | 'DATABASE DEL', self._quote(family), self._quote(key)) 668 | res, value = result['result'] 669 | if res == '0': 670 | raise AGIDBError('Unable to delete from database: family=%s, key=%s' % (family, key)) 671 | 672 | def database_deltree(self, family, key=''): 673 | """agi.database_deltree(family, key='') --> None 674 | Deletes a family or specific keytree with in a family 675 | in the Asterisk database. 676 | """ 677 | result = self.execute( 678 | 'DATABASE DELTREE', self._quote(family), self._quote(key)) 679 | res, value = result['result'] 680 | if res == '0': 681 | raise AGIDBError('Unable to delete tree from database: family=%s, key=%s' % (family, key)) 682 | 683 | def noop(self): 684 | """agi.noop() --> None 685 | Does nothing 686 | """ 687 | self.execute('NOOP') 688 | 689 | def exec_command(self, command, *args): 690 | """Send an arbitrary asterisk command with args (even not AGI commands)""" 691 | # The arguments of the command should be prepared as comma delimited, that's the way the EXEC works 692 | args = ','.join(map(str, args)) 693 | return self.execute('EXEC', command, args) 694 | 695 | 696 | if __name__ == '__main__': 697 | agi = AGI() 698 | #agi.appexec('festival','Welcome to Klass Technologies. Thank you for calling.') 699 | #agi.appexec('festival','This is a test of the text to speech engine.') 700 | #agi.appexec('festival','Press 1 for sales ') 701 | #agi.appexec('festival','Press 2 for customer support ') 702 | #agi.hangup() 703 | #agi.goto_on_exit(extension='1234', priority='1') 704 | #sys.exit(0) 705 | #agi.say_digits('123', [4,'5',6]) 706 | #agi.say_digits([4,5,6]) 707 | #agi.say_number('1234') 708 | #agi.say_number('01234') # 668 709 | #agi.say_number('0xf5') # 245 710 | agi.get_data('demo-congrats') 711 | agi.hangup() 712 | sys.exit(0) 713 | #agi.record_file('pyst-test') #FAILS 714 | #agi.stream_file('demo-congrats', [1,2,3,4,5,6,7,8,9,0,'#','*']) 715 | #agi.appexec('background','demo-congrats') 716 | 717 | try: 718 | agi.appexec('backgrounder', 'demo-congrats') 719 | except AGIAppError: 720 | sys.stderr.write( 721 | "Handled exception for missing application backgrounder\n") 722 | 723 | agi.set_variable('foo', 'bar') 724 | agi.get_variable('foo') 725 | 726 | try: 727 | agi.get_variable('foobar') 728 | except AGIAppError: 729 | sys.stderr.write("Handled exception for missing variable foobar\n") 730 | 731 | try: 732 | agi.database_put('foo', 'bar', 'foobar') 733 | agi.database_put('foo', 'baz', 'foobaz') 734 | agi.database_put('foo', 'bat', 'foobat') 735 | v = agi.database_get('foo', 'bar') 736 | sys.stderr.write('DBVALUE foo:bar = %s\n' % v) 737 | v = agi.database_get('bar', 'foo') 738 | sys.stderr.write('DBVALUE foo:bar = %s\n' % v) 739 | agi.database_del('foo', 'bar') 740 | agi.database_deltree('foo') 741 | except AGIDBError: 742 | sys.stderr.write( 743 | "Handled exception for missing database entry bar:foo\n") 744 | 745 | agi.hangup() 746 | -------------------------------------------------------------------------------- /asterisk/agitb.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. module:: agitb 3 | :synopsis: More comprehensive traceback formatting for Python scripts. 4 | 5 | Example 6 | ------- 7 | 8 | To enable this module, do: 9 | 10 | .. code-block:: python 11 | 12 | import asterisk.agitb, asterisk.agi 13 | asterisk.agitb.enable(display = False, logdir = '/var/log/asterisk/') 14 | 15 | agi = asterisk.agi.AGI() 16 | asterisk.agitb.enable(agi, False, '/var/log/asterisk') 17 | 18 | at the top of your script. The optional arguments to enable() are: 19 | 20 | * agi - the agi handle to write verbose messages to 21 | * display - if true, tracebacks are displayed on the asterisk console 22 | (used with the agi option) 23 | * logdir - if set, tracebacks are written to files in this directory 24 | * context - number of lines of source code to show for each stack frame 25 | 26 | By default, tracebacks are displayed but not saved, and the context is 5 lines. 27 | 28 | You may want to add a logdir if you call agitb.enable() before you have 29 | an agi.AGI() handle. 30 | 31 | Alternatively, if you have caught an exception and want agitb to display it 32 | for you, call agitb.handler(). The optional argument to handler() is a 33 | 3-item tuple (etype, evalue, etb) just like the value of sys.exc_info(). 34 | If you do not pass anything to handler() it will use sys.exc_info(). 35 | 36 | This script was adapted from Ka-Ping Yee's cgitb. 37 | 38 | Specification 39 | ------------- 40 | """ 41 | 42 | __author__ = 'Matthew Nicholson' 43 | __version__ = '0.1.0' 44 | 45 | import sys 46 | 47 | __UNDEF__ = [] # a special sentinel object 48 | 49 | 50 | def lookup(name, frame, locals): 51 | """Find the value for a given name in the given environment.""" 52 | if name in locals: 53 | return 'local', locals[name] 54 | if name in frame.f_globals: 55 | return 'global', frame.f_globals[name] 56 | if '__builtins__' in frame.f_globals: 57 | builtins = frame.f_globals['__builtins__'] 58 | if isinstance(builtins, type({})): 59 | if name in builtins: 60 | return 'builtin', builtins[name] 61 | else: 62 | if hasattr(builtins, name): 63 | return 'builtin', getattr(builtins, name) 64 | return None, __UNDEF__ 65 | 66 | 67 | def scanvars(reader, frame, locals): 68 | """Scan one logical line of Python and look up values of variables used.""" 69 | import tokenize 70 | import keyword 71 | vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__ 72 | for ttype, token, start, end, line in tokenize.generate_tokens(reader): 73 | if ttype == tokenize.NEWLINE: 74 | break 75 | if ttype == tokenize.NAME and token not in keyword.kwlist: 76 | if lasttoken == '.': 77 | if parent is not __UNDEF__: 78 | value = getattr(parent, token, __UNDEF__) 79 | vars.append((prefix + token, prefix, value)) 80 | else: 81 | where, value = lookup(token, frame, locals) 82 | vars.append((token, where, value)) 83 | elif token == '.': 84 | prefix += lasttoken + '.' 85 | parent = value 86 | else: 87 | parent, prefix = None, '' 88 | lasttoken = token 89 | return vars 90 | 91 | 92 | def text(eparams, context=5): 93 | """Return a plain text document describing a given traceback.""" 94 | import os 95 | import types 96 | import time 97 | import traceback 98 | import linecache 99 | import inspect 100 | import pydoc 101 | 102 | etype, evalue, etb = eparams 103 | if isinstance(etype, types.ClassType): 104 | etype = etype.__name__ 105 | pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable 106 | date = time.ctime(time.time()) 107 | head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + ''' 108 | A problem occurred in a Python script. Here is the sequence of 109 | function calls leading up to the error, in the order they occurred. 110 | ''' 111 | 112 | frames = [] 113 | records = inspect.getinnerframes(etb, context) 114 | for frame, file, lnum, func, lines, index in records: 115 | file = file and os.path.abspath(file) or '?' 116 | args, varargs, varkw, locals = inspect.getargvalues(frame) 117 | call = '' 118 | if func != '?': 119 | call = 'in ' + func + \ 120 | inspect.formatargvalues(args, varargs, varkw, locals, 121 | formatvalue=lambda value: '=' + pydoc.text.repr(value)) 122 | 123 | highlight = {} 124 | 125 | def reader(lnum=[lnum]): 126 | highlight[lnum[0]] = 1 127 | try: 128 | return linecache.getline(file, lnum[0]) 129 | finally: 130 | lnum[0] += 1 131 | vars = scanvars(reader, frame, locals) 132 | 133 | rows = [' %s %s' % (file, call)] 134 | if index is not None: 135 | i = lnum - index 136 | for line in lines: 137 | num = '%5d ' % i 138 | rows.append(num + line.rstrip()) 139 | i += 1 140 | 141 | done, dump = {}, [] 142 | for name, where, value in vars: 143 | if name in done: 144 | continue 145 | done[name] = 1 146 | if value is not __UNDEF__: 147 | if where == 'global': 148 | name = 'global ' + name 149 | elif where == 'local': 150 | name = name 151 | else: 152 | name = where + name.split('.')[-1] 153 | dump.append('%s = %s' % (name, pydoc.text.repr(value))) 154 | else: 155 | dump.append(name + ' undefined') 156 | 157 | rows.append('\n'.join(dump)) 158 | frames.append('\n%s\n' % '\n'.join(rows)) 159 | 160 | exception = ['%s: %s' % (str(etype), str(evalue))] 161 | if isinstance(evalue, types.InstanceType): 162 | for name in dir(evalue): 163 | value = pydoc.text.repr(getattr(evalue, name)) 164 | exception.append('\n%s%s = %s' % (" " * 4, name, value)) 165 | 166 | import traceback 167 | return head + ''.join(frames) + ''.join(exception) + ''' 168 | 169 | The above is a description of an error in a Python program. Here is 170 | the original traceback: 171 | 172 | %s 173 | ''' % ''.join(traceback.format_exception(etype, evalue, etb)) 174 | 175 | 176 | class Hook: 177 | """A hook to replace sys.excepthook that shows tracebacks in HTML.""" 178 | 179 | def __init__(self, display=1, logdir=None, context=5, file=None, 180 | agi=None): 181 | self.display = display # send tracebacks to browser if true 182 | self.logdir = logdir # log tracebacks to files if not None 183 | self.context = context # number of source code lines per frame 184 | self.file = file or sys.stderr # place to send the output 185 | self.agi = agi 186 | 187 | def __call__(self, etype, evalue, etb): 188 | self.handle((etype, evalue, etb)) 189 | 190 | def handle(self, info=None): 191 | info = info or sys.exc_info() 192 | 193 | try: 194 | doc = text(info, self.context) 195 | except: # just in case something goes wrong 196 | import traceback 197 | doc = ''.join(traceback.format_exception(*info)) 198 | 199 | if self.display: 200 | if self.agi: # print to agi 201 | for line in doc.split('\n'): 202 | self.agi.verbose(line, 4) 203 | else: 204 | self.file.write(doc + '\n') 205 | 206 | if self.agi: 207 | self.agi.verbose('A problem occured in a python script', 4) 208 | else: 209 | self.file.write('A problem occured in a python script\n') 210 | 211 | if self.logdir is not None: 212 | import os 213 | import tempfile 214 | (fd, path) = tempfile.mkstemp(suffix='.txt', dir=self.logdir) 215 | try: 216 | file = os.fdopen(fd, 'w') 217 | file.write(doc) 218 | file.close() 219 | msg = '%s contains the description of this error.' % path 220 | except: 221 | msg = 'Tried to save traceback to %s, but failed.' % path 222 | 223 | if self.agi: 224 | self.agi.verbose(msg, 4) 225 | else: 226 | self.file.write(msg + '\n') 227 | 228 | try: 229 | self.file.flush() 230 | except: 231 | pass 232 | 233 | 234 | handler = Hook().handle 235 | 236 | 237 | def enable(agi=None, display=1, logdir=None, context=5): 238 | """Install an exception handler that formats tracebacks as HTML. 239 | 240 | The optional argument 'display' can be set to 0 to suppress sending the 241 | traceback to the browser, and 'logdir' can be set to a directory to cause 242 | tracebacks to be written to files there.""" 243 | except_hook = Hook(display=display, logdir=logdir, 244 | context=context, agi=agi) 245 | sys.excepthook = except_hook 246 | 247 | global handler 248 | handler = except_hook.handle 249 | -------------------------------------------------------------------------------- /asterisk/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set expandtab: 3 | """ 4 | .. module:: config 5 | :synopsis: Parse Asterisk configuration files. 6 | 7 | This module provides parsing functionality for asterisk config files. 8 | 9 | Example 10 | ---------- 11 | 12 | .. code-block:: python 13 | 14 | import asterisk.config 15 | import sys 16 | 17 | # load and parse the config file 18 | try: 19 | config = asterisk.config.Config('/etc/asterisk/extensions.conf') 20 | except asterisk.config.ParseError as e: 21 | print "Parse Error line: %s: %s" % (e.line, e.strerror) 22 | sys.exit(1) 23 | except IOError as e: 24 | print "Error opening file: %s" % e.strerror 25 | sys.exit(1) 26 | 27 | # print our parsed output 28 | for category in config.categories: 29 | print '[%s]' % category.name # print the current category 30 | 31 | for item in category.items: 32 | print ' %s = %s' % (item.name, item.value) 33 | 34 | 35 | Specification 36 | ------------- 37 | """ 38 | 39 | import sys 40 | 41 | 42 | class ParseError(Exception): 43 | pass 44 | 45 | 46 | class Line(object): 47 | def __init__(self, line, number): 48 | self.line = '' 49 | self.comment = '' 50 | line = line.strip() # I guess we don't preserve indentation 51 | self.number = number 52 | parts = line.split(';') 53 | if len(parts) >= 2: 54 | self.line = parts[0].strip() 55 | self.comment = ';'.join( 56 | parts[1:]) # Just in case the comment contained ';' 57 | else: 58 | self.line = line 59 | 60 | def __str__(self): 61 | return self.get_line() 62 | 63 | def get_line(self): 64 | if self.comment and self.line: 65 | return '%s\t;%s' % (self.line, self.comment) 66 | elif self.comment and not self.line: 67 | return ';%s' % self.comment 68 | return self.line 69 | 70 | 71 | class Category(Line): 72 | def __init__(self, line='', num=-1, name=None): 73 | Line.__init__(self, line, num) 74 | if self.line: 75 | if (self.line[0] != '[' or self.line[-1] != ']'): 76 | raise ParseError( 77 | self.number, "Missing '[' or ']' in category definition") 78 | self.name = self.line[1:-1] 79 | elif name: 80 | self.name = name 81 | else: 82 | raise Exception( 83 | "Must provide name or line representing a category") 84 | 85 | self.items = [] 86 | self.comments = [] 87 | 88 | def get_line(self): 89 | if self.comment: 90 | return '[%s]\t;%s' % (self.name, self.comment) 91 | return '[%s]' % self.name 92 | 93 | def append(self, item): 94 | self.items.append(item) 95 | 96 | def insert(self, index, item): 97 | self.items.insert(index, item) 98 | 99 | def pop(self, index=-1): 100 | self.items.pop(index) 101 | 102 | def remove(self, item): 103 | self.items.remove(item) 104 | 105 | 106 | class Item(Line): 107 | def __init__(self, line='', num=-1, name=None, value=None): 108 | Line.__init__(self, line, num) 109 | self.style = '' 110 | if self.line: 111 | self.parse() 112 | elif (name and value): 113 | self.name = name 114 | self.value = value 115 | else: 116 | raise Exception("Must provide name or value representing an item") 117 | 118 | def parse(self): 119 | try: 120 | name, value = self.line.split('=', 1) 121 | except ValueError: 122 | if self.line.strip()[-1] == ']': 123 | raise ParseError(self.number, "Category name missing '['") 124 | else: 125 | raise ParseError( 126 | self.number, "Item must be in name = value pairs") 127 | 128 | if value and value[0] == '>': 129 | self.style = '>' # preserve the style of the original 130 | value = value[1:].strip() 131 | self.name = name.strip() 132 | self.value = value 133 | 134 | def get_line(self): 135 | if self.comment: 136 | return '%s =%s %s\t;%s' % (self.name, self.style, self.value, self.comment) 137 | return '%s =%s %s' % (self.name, self.style, self.value) 138 | 139 | 140 | class Config(object): 141 | def __init__(self, filename): 142 | self.filename = filename 143 | self.raw_lines = [] # Holds the raw strings 144 | self.lines = [] # Holds things in order 145 | self.categories = [] 146 | 147 | # load and parse the file 148 | self.load() 149 | self.parse() 150 | 151 | def load(self): 152 | self.raw_lines = open(self.filename).readlines() 153 | #try: 154 | #self.raw_lines = open(self.filename).readlines() 155 | #except IOError: 156 | #sys.stderr.write('WARNING: error opening filename: %s No data read. Starting new file?' % self.filename) 157 | #self.raw_lines = [] 158 | 159 | def parse(self): 160 | cat = None 161 | num = 0 162 | for line in self.raw_lines: 163 | num += 1 164 | line = line.strip() 165 | if not line or line[0] == ';': 166 | item = Line(line or '', num) 167 | self.lines.append(item) 168 | if cat: 169 | cat.comments.append(item) 170 | continue 171 | elif line[0] == '[': 172 | cat = Category(line, num) 173 | self.lines.append(cat) 174 | self.categories.append(cat) 175 | continue 176 | else: 177 | item = Item(line, num) 178 | self.lines.append(item) 179 | if cat: 180 | cat.append(item) 181 | continue 182 | -------------------------------------------------------------------------------- /asterisk/fastagi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | .. module:: fastagi 5 | :synopsis: FastAGI service for Asterisk 6 | 7 | Requires modified pyst2 to support reading stdin/out/err 8 | 9 | Copyright 2011 VOICE1, LLC 10 | By: Ben Davis 11 | 12 | Specification 13 | ------------- 14 | """ 15 | 16 | import sys 17 | import SocketServer 18 | import asterisk.agi 19 | # import pkg_resources 20 | # PYST_VERSION = pkg_resources.get_distribution("pyst2").version 21 | 22 | __verison__ = 0.1 23 | 24 | #TODO: Read options from config file. 25 | HOST, PORT = "127.0.0.1", 4573 26 | 27 | class FastAGI(SocketServer.StreamRequestHandler): 28 | # Close connections not finished in 5seconds. 29 | timeout = 5 30 | def handle(self): 31 | try: 32 | agi=asterisk.agi.AGI(stdin=self.rfile, stdout=self.wfile, stderr=sys.stderr) 33 | agi.verbose("pyst2: FastAGI on: {}:{}".format(HOST, PORT)) 34 | except TypeError as e: 35 | sys.stderr.write('Unable to connect to agi://{} {}\n'.format(self.client_address[0], str(e))) 36 | except SocketServer.socket.timeout as e: 37 | sys.stderr.write('Timeout receiving data from {}\n'.format(self.client_address)) 38 | except SocketServer.socket.error as e: 39 | sys.stderr.write('Could not open the socket. Is someting else listening on this port?\n') 40 | except Exception as e: 41 | sys.stderr.write('An unknown error: {}\n'.format(str(e))) 42 | 43 | if __name__ == "__main__": 44 | # server = SocketServer.TCPServer((HOST, PORT), FastAGI) 45 | server = SocketServer.ForkingTCPServer((HOST, PORT), FastAGI) 46 | 47 | # Keep server running until CTRL-C is pressed. 48 | server.serve_forever() 49 | -------------------------------------------------------------------------------- /asterisk/manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set expandtab shiftwidth=4: 3 | 4 | """ 5 | .. module:: manager 6 | :synopsis: Python Interface for Asterisk Manager 7 | 8 | This module provides a Python API for interfacing with the asterisk manager. 9 | 10 | Example 11 | ------- 12 | 13 | .. code-block:: python 14 | 15 | import asterisk.manager 16 | import sys 17 | 18 | def handle_shutdown(event, manager): 19 | print "Recieved shutdown event" 20 | manager.close() 21 | # we could analize the event and reconnect here 22 | 23 | def handle_event(event, manager): 24 | print "Recieved event: %s" % event.name 25 | 26 | manager = asterisk.manager.Manager() 27 | try: 28 | # connect to the manager 29 | try: 30 | manager.connect('host') 31 | manager.login('user', 'secret') 32 | 33 | # register some callbacks 34 | manager.register_event('Shutdown', handle_shutdown) # shutdown 35 | manager.register_event('*', handle_event) # catch all 36 | 37 | # get a status report 38 | response = manager.status() 39 | 40 | manager.logoff() 41 | except asterisk.manager.ManagerSocketException as e: 42 | print "Error connecting to the manager: %s" % e.strerror 43 | sys.exit(1) 44 | except asterisk.manager.ManagerAuthException as e: 45 | print "Error logging in to the manager: %s" % e.strerror 46 | sys.exit(1) 47 | except asterisk.manager.ManagerException as e: 48 | print "Error: %s" % e.strerror 49 | sys.exit(1) 50 | 51 | finally: 52 | # remember to clean up 53 | manager.close() 54 | 55 | Remember all header, response, and event names are case sensitive. 56 | 57 | Not all manager actions are implmented as of yet, feel free to add them 58 | and submit patches. 59 | 60 | Specification 61 | ------------- 62 | """ 63 | 64 | import sys 65 | import os 66 | import socket 67 | import threading 68 | import uuid 69 | from six import PY3 70 | from six.moves import queue 71 | import re 72 | from types import * 73 | from time import sleep 74 | 75 | EOL = '\r\n' 76 | 77 | 78 | class ManagerMsg(object): 79 | """A manager interface message""" 80 | def __init__(self, response): 81 | # the raw response, straight from the horse's mouth: 82 | self.response = response 83 | self.data = '' 84 | self.headers = {} 85 | 86 | # parse the response 87 | self.parse(response) 88 | 89 | # This is an unknown message, may happen if a command (notably 90 | # 'dialplan show something') contains a \n\r\n sequence in the 91 | # middle of output. We hope this happens only *once* during a 92 | # misbehaved command *and* the command ends with --END COMMAND-- 93 | # in that case we return an Event. Otherwise we asume it is 94 | # from a misbehaving command not returning a proper header (e.g. 95 | # IAXnetstats in Asterisk 1.4.X) 96 | # A better solution is probably to retain some knowledge of 97 | # commands sent and their expected return syntax. In that case 98 | # we could wait for --END COMMAND-- for 'command'. 99 | # B0rken in asterisk. This should be parseable without context. 100 | if 'Event' not in self.headers and 'Response' not in self.headers: 101 | # there are commands that return the ActionID but not 102 | # 'Response', e.g., IAXpeers in Asterisk 1.4.X 103 | if self.has_header('ActionID'): 104 | self.headers['Response'] = 'Generated Header' 105 | elif '--END COMMAND--' in self.data: 106 | self.headers['Event'] = 'NoClue' 107 | else: 108 | self.headers['Response'] = 'Generated Header' 109 | 110 | def parse(self, response): 111 | """Parse a manager message""" 112 | 113 | data = [] 114 | for n, line in enumerate(response): 115 | # all valid header lines end in \r\n in Asterisk<=13 116 | # and all valid headers lines in Asterisk>13 dont's starts 117 | # with 'Output:' 118 | if not line.endswith('\r\n') or line.startswith('Output:'): 119 | data.extend(response[n:]) 120 | break 121 | try: 122 | k, v = (x.strip() for x in line.split(':', 1)) 123 | # if header is ChanVariable it can have more that one value 124 | # we store the variable in a dictionary parsed 125 | if 'ChanVariable' in k: 126 | if not self.headers.has_key('ChanVariable'): 127 | self.headers['ChanVariable']={} 128 | name, value = (x.strip() for x in v.split('=',1)) 129 | self.headers['ChanVariable'][name]=value 130 | else: 131 | self.headers[k] = v 132 | except ValueError: 133 | # invalid header, start of multi-line data response 134 | data.extend(response[n:]) 135 | break 136 | self.data = ''.join(data) 137 | 138 | def has_header(self, hname): 139 | """Check for a header""" 140 | return hname in self.headers 141 | 142 | def get_header(self, hname, defval=None): 143 | """Return the specified header""" 144 | return self.headers.get(hname, defval) 145 | 146 | def __getitem__(self, hname): 147 | """Return the specified header""" 148 | return self.headers[hname] 149 | 150 | def __repr__(self): 151 | if 'Response' in self.headers: 152 | return self.headers['Response'] 153 | else: 154 | return self.headers['Event'] 155 | 156 | 157 | class Event(object): 158 | """Manager interface Events, __init__ expects a 'ManagerMsg' message""" 159 | def __init__(self, message): 160 | 161 | # store all of the event data 162 | self.message = message 163 | self.data = message.data 164 | self.headers = message.headers 165 | 166 | # if this is not an event message we have a problem 167 | if not message.has_header('Event'): 168 | raise ManagerException( 169 | 'Trying to create event from non event message') 170 | 171 | # get the event name 172 | self.name = message.get_header('Event') 173 | 174 | def has_header(self, hname): 175 | """Check for a header""" 176 | return hname in self.headers 177 | 178 | def get_header(self, hname, defval=None): 179 | """Return the specified header""" 180 | return self.headers.get(hname, defval) 181 | 182 | def __getitem__(self, hname): 183 | """Return the specified header""" 184 | return self.headers[hname] 185 | 186 | def __repr__(self): 187 | return self.headers['Event'] 188 | 189 | def get_action_id(self): 190 | return self.headers.get('ActionID', 0000) 191 | 192 | 193 | class Manager(object): 194 | def __init__(self): 195 | self._sock = None # our socket 196 | self.title = None # set by received greeting 197 | self._connected = threading.Event() 198 | self._running = threading.Event() 199 | 200 | # our hostname 201 | self.hostname = socket.gethostname() 202 | self.actionID_base = str(uuid.uuid4()) 203 | 204 | # our queues 205 | self._message_queue = queue.Queue() 206 | self._response_queue = queue.Queue() 207 | self._event_queue = queue.Queue() 208 | 209 | # callbacks for events 210 | self._event_callbacks = {} 211 | 212 | self._reswaiting = [] # who is waiting for a response 213 | 214 | # sequence stuff 215 | self._seqlock = threading.Lock() 216 | self._seq = 0 217 | 218 | # some threads 219 | self.message_thread = threading.Thread(target=self.message_loop) 220 | self.event_dispatch_thread = threading.Thread( 221 | target=self.event_dispatch) 222 | 223 | self.message_thread.setDaemon(True) 224 | self.event_dispatch_thread.setDaemon(True) 225 | 226 | def __del__(self): 227 | self.close() 228 | 229 | def connected(self): 230 | """ 231 | Check if we are connected or not. 232 | """ 233 | return self._connected.isSet() 234 | 235 | def next_seq(self): 236 | """Return the next number in the sequence, this is used for ActionID""" 237 | self._seqlock.acquire() 238 | try: 239 | return self._seq 240 | finally: 241 | self._seq += 1 242 | self._seqlock.release() 243 | 244 | def get_actionID(self): 245 | """ 246 | Teturn an unique actionID, with a shared prefix for all actionIDs 247 | generated by this Manager instance """ 248 | return '%s-%08x' % (self.actionID_base, self.next_seq()) 249 | 250 | def send_action(self, cdict={}, **kwargs): 251 | """ 252 | Send a command to the manager 253 | 254 | If a list is passed to the cdict argument, each item in the list will 255 | be sent to asterisk under the same header in the following manner: 256 | 257 | cdict = {"Action": "Originate", 258 | "Variable": ["var1=value", "var2=value"]} 259 | send_action(cdict) 260 | 261 | ... 262 | 263 | Action: Originate 264 | Variable: var1=value 265 | Variable: var2=value 266 | """ 267 | 268 | if not self._connected.isSet(): 269 | raise ManagerException("Not connected") 270 | 271 | # fill in our args 272 | cdict.update(kwargs) 273 | 274 | # set the action id 275 | if 'ActionID' not in cdict: 276 | cdict['ActionID'] = self.get_actionID() 277 | clist = [] 278 | 279 | # generate the command 280 | for key, value in cdict.items(): 281 | if isinstance(value, list): 282 | for item in value: 283 | item = tuple([key, item]) 284 | clist.append('%s: %s' % item) 285 | else: 286 | item = tuple([key, value]) 287 | clist.append('%s: %s' % item) 288 | clist.append(EOL) 289 | command = EOL.join(clist) 290 | 291 | # lock the socket and send our command 292 | try: 293 | self._sock.write(command.encode('utf8','ignore')) 294 | # Check if The socket is already closed. May happen after sending "Action: Logoff" 295 | if not self._sock.closed: 296 | self._sock.flush() 297 | except socket.error as e: 298 | raise ManagerSocketException(e.errno, e.strerror) 299 | 300 | self._reswaiting.insert(0, 1) 301 | response = self._response_queue.get() 302 | self._reswaiting.pop(0) 303 | 304 | if not response: 305 | raise ManagerSocketException(0, 'Connection Terminated') 306 | 307 | return response 308 | 309 | def _receive_data(self): 310 | """ 311 | Read the response from a command. 312 | """ 313 | 314 | multiline = False 315 | status = False 316 | wait_for_marker = False 317 | eolcount = 0 318 | # loop while we are still running and connected 319 | while self._running.isSet() and self._connected.isSet(): 320 | try: 321 | lines = [] 322 | for line in self._sock: 323 | line = line.decode('utf8','ignore') 324 | # check to see if this is the greeting line 325 | if not self.title and '/' in line and not ':' in line: 326 | # store the title of the manager we are connecting to: 327 | self.title = line.split('/')[0].strip() 328 | # store the version of the manager we are connecting to: 329 | self.version = line.split('/')[1].strip() 330 | # fake message header 331 | lines.append('Response: Generated Header\r\n') 332 | lines.append(line) 333 | break 334 | # If the line is EOL marker we have a complete message. 335 | # Some commands are broken and contain a \n\r\n 336 | # sequence, in the case wait_for_marker is set, we 337 | # have such a command where the data ends with the 338 | # marker --END COMMAND--, so we ignore embedded 339 | # newlines until we see that marker 340 | if line == EOL and not wait_for_marker: 341 | multiline = False 342 | if lines or not self._connected.isSet(): 343 | break 344 | # ignore empty lines at start 345 | continue 346 | # If the user executed the status command, it's a special 347 | # case, so we need to look for a marker. 348 | if 'status will follow' in line: 349 | status = True 350 | wait_for_marker = True 351 | lines.append(line) 352 | 353 | # line not ending in \r\n or without ':' isn't a 354 | # valid header and starts multiline response 355 | if not line.endswith('\r\n') or ':' not in line: 356 | multiline = True 357 | # Response: Follows indicates we should wait for end 358 | # marker --END COMMAND-- 359 | if not (multiline or status) and line.startswith('Response') and \ 360 | line.split(':', 1)[1].strip() == 'Follows': 361 | wait_for_marker = True 362 | # same when seeing end of multiline response 363 | if multiline and (line.startswith('--END COMMAND--') or line.strip().endswith('--END COMMAND--')): 364 | wait_for_marker = False 365 | multiline = False 366 | # same when seeing end of status response 367 | if status and 'StatusComplete' in line: 368 | wait_for_marker = False 369 | status = False 370 | if not self._connected.isSet(): 371 | break 372 | else: 373 | # EOF during reading 374 | self._sock.close() 375 | self._connected.clear() 376 | # if we have a message append it to our queue 377 | if lines and self._connected.isSet(): 378 | self._message_queue.put(lines) 379 | else: 380 | self._message_queue.put(None) 381 | except socket.error: 382 | self._sock.close() 383 | self._connected.clear() 384 | self._message_queue.put(None) 385 | 386 | def register_event(self, event, function): 387 | """ 388 | Register a callback for the specified event. 389 | If a callback function returns True, no more callbacks for that 390 | event will be executed. 391 | """ 392 | 393 | # get the current value, or an empty list 394 | # then add our new callback 395 | current_callbacks = self._event_callbacks.get(event, []) 396 | current_callbacks.append(function) 397 | self._event_callbacks[event] = current_callbacks 398 | 399 | def unregister_event(self, event, function): 400 | """ 401 | Unregister a callback for the specified event. 402 | """ 403 | current_callbacks = self._event_callbacks.get(event, []) 404 | current_callbacks.remove(function) 405 | self._event_callbacks[event] = current_callbacks 406 | 407 | def message_loop(self): 408 | """ 409 | The method for the event thread. 410 | This actually recieves all types of messages and places them 411 | in the proper queues. 412 | """ 413 | 414 | # start a thread to recieve data 415 | t = threading.Thread(target=self._receive_data) 416 | t.setDaemon(True) 417 | t.start() 418 | 419 | try: 420 | # loop getting messages from the queue 421 | while self._running.isSet(): 422 | # get/wait for messages 423 | data = self._message_queue.get() 424 | 425 | # if we got None as our message we are done 426 | if not data: 427 | # notify the other queues 428 | self._event_queue.put(None) 429 | for waiter in self._reswaiting: 430 | self._response_queue.put(None) 431 | break 432 | 433 | # parse the data 434 | message = ManagerMsg(data) 435 | 436 | # check if this is an event message 437 | if message.has_header('Event'): 438 | self._event_queue.put(Event(message)) 439 | # check if this is a response 440 | elif message.has_header('Response'): 441 | self._response_queue.put(message) 442 | else: 443 | print('No clue what we got\n%s' % message.data) 444 | finally: 445 | # wait for our data receiving thread to exit 446 | t.join() 447 | 448 | def event_dispatch(self): 449 | """This thread is responsible for dispatching events""" 450 | 451 | # loop dispatching events 452 | while self._running.isSet(): 453 | # get/wait for an event 454 | ev = self._event_queue.get() 455 | 456 | # if we got None as an event, we are finished 457 | if not ev: 458 | break 459 | 460 | # dispatch our events 461 | 462 | # first build a list of the functions to execute 463 | callbacks = (self._event_callbacks.get(ev.name, []) 464 | + self._event_callbacks.get('*', [])) 465 | 466 | # now execute the functions 467 | for callback in callbacks: 468 | if callback(ev, self): 469 | break 470 | 471 | def connect(self, host, port=5038, buffer_size=0): 472 | """Connect to the manager interface""" 473 | 474 | if self._connected.isSet(): 475 | raise ManagerException('Already connected to manager') 476 | 477 | # make sure host is a string 478 | assert type(host) is str 479 | 480 | port = int(port) # make sure port is an int 481 | buffer_size = int(buffer_size) # make sure buffer_siz is an int as well 482 | 483 | # create our socket and connect 484 | try: 485 | _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 486 | _sock.connect((host, port)) 487 | if PY3: 488 | self._sock = _sock.makefile(mode='rwb', buffering=buffer_size) 489 | else: 490 | self._sock = _sock.makefile() 491 | _sock.close() 492 | except socket.error as e: 493 | raise ManagerSocketException(e.errno, e.strerror) 494 | 495 | # we are connected and running 496 | self._connected.set() 497 | self._running.set() 498 | 499 | # start the event thread 500 | self.message_thread.start() 501 | 502 | # start the event dispatching thread 503 | self.event_dispatch_thread.start() 504 | 505 | # get our initial connection response 506 | return self._response_queue.get() 507 | 508 | def close(self): 509 | """Shutdown the connection to the manager""" 510 | 511 | # if we are still running, logout 512 | if self._running.isSet() and self._connected.isSet(): 513 | self.logoff() 514 | 515 | if self._running.isSet(): 516 | # put None in the message_queue to kill our threads 517 | self._message_queue.put(None) 518 | 519 | # wait for the event thread to exit 520 | self.message_thread.join() 521 | 522 | # make sure we do not join our self (when close is called from event handlers) 523 | if threading.currentThread() != self.event_dispatch_thread: 524 | # wait for the dispatch thread to exit 525 | self.event_dispatch_thread.join() 526 | 527 | self._running.clear() 528 | 529 | # Manager actions 530 | 531 | def login(self, username, secret): 532 | """Login to the manager, throws ManagerAuthException when login falis""" 533 | 534 | cdict = {'Action': 'Login'} 535 | cdict['Username'] = username 536 | cdict['Secret'] = secret 537 | response = self.send_action(cdict) 538 | 539 | if response.get_header('Response') == 'Error': 540 | raise ManagerAuthException(response.get_header('Message')) 541 | 542 | return response 543 | 544 | def ping(self): 545 | """Send a ping action to the manager""" 546 | cdict = {'Action': 'Ping'} 547 | response = self.send_action(cdict) 548 | return response 549 | 550 | def logoff(self): 551 | """Logoff from the manager""" 552 | 553 | cdict = {'Action': 'Logoff'} 554 | response = self.send_action(cdict) 555 | 556 | return response 557 | 558 | def hangup(self, channel): 559 | """Hangup the specified channel""" 560 | 561 | cdict = {'Action': 'Hangup'} 562 | cdict['Channel'] = channel 563 | response = self.send_action(cdict) 564 | 565 | return response 566 | 567 | def status(self, channel=''): 568 | """Get a status message from asterisk""" 569 | 570 | cdict = {'Action': 'Status'} 571 | cdict['Channel'] = channel 572 | response = self.send_action(cdict) 573 | 574 | return response 575 | 576 | def redirect(self, channel, exten, priority='1', extra_channel='', context=''): 577 | """Redirect a channel""" 578 | 579 | cdict = {'Action': 'Redirect'} 580 | cdict['Channel'] = channel 581 | cdict['Exten'] = exten 582 | cdict['Priority'] = priority 583 | if context: 584 | cdict['Context'] = context 585 | if extra_channel: 586 | cdict['ExtraChannel'] = extra_channel 587 | response = self.send_action(cdict) 588 | 589 | return response 590 | 591 | def originate(self, channel, exten, context='', priority='', timeout='', application='', data='', caller_id='', run_async=False, earlymedia='false', account='', variables={}): 592 | """Originate a call""" 593 | 594 | cdict = {'Action': 'Originate'} 595 | cdict['Channel'] = channel 596 | cdict['Exten'] = exten 597 | if context: 598 | cdict['Context'] = context 599 | if priority: 600 | cdict['Priority'] = priority 601 | if timeout: 602 | cdict['Timeout'] = timeout 603 | if application: 604 | cdict['Application'] = application 605 | if data: 606 | cdict['Data'] = data 607 | if caller_id: 608 | cdict['CallerID'] = caller_id 609 | if run_async: 610 | cdict['Async'] = 'yes' 611 | if earlymedia: 612 | cdict['EarlyMedia'] = earlymedia 613 | if account: 614 | cdict['Account'] = account 615 | # join dict of vairables together in a string in the form of 'key=val|key=val' 616 | # with the latest CVS HEAD this is no longer necessary 617 | # if variables: cdict['Variable'] = '|'.join(['='.join((str(key), str(value))) for key, value in variables.items()]) 618 | if variables: 619 | cdict['Variable'] = ['='.join( 620 | (str(key), str(value))) for key, value in variables.items()] 621 | 622 | response = self.send_action(cdict) 623 | 624 | return response 625 | 626 | def mailbox_status(self, mailbox): 627 | """Get the status of the specified mailbox""" 628 | 629 | cdict = {'Action': 'MailboxStatus'} 630 | cdict['Mailbox'] = mailbox 631 | response = self.send_action(cdict) 632 | 633 | return response 634 | 635 | def command(self, command): 636 | """Execute a command""" 637 | 638 | cdict = {'Action': 'Command'} 639 | cdict['Command'] = command 640 | response = self.send_action(cdict) 641 | 642 | return response 643 | 644 | def extension_state(self, exten, context): 645 | """Get the state of an extension""" 646 | 647 | cdict = {'Action': 'ExtensionState'} 648 | cdict['Exten'] = exten 649 | cdict['Context'] = context 650 | response = self.send_action(cdict) 651 | 652 | return response 653 | 654 | def playdtmf(self, channel, digit): 655 | """Plays a dtmf digit on the specified channel""" 656 | 657 | cdict = {'Action': 'PlayDTMF'} 658 | cdict['Channel'] = channel 659 | cdict['Digit'] = digit 660 | response = self.send_action(cdict) 661 | 662 | return response 663 | 664 | def absolute_timeout(self, channel, timeout): 665 | """Set an absolute timeout on a channel""" 666 | 667 | cdict = {'Action': 'AbsoluteTimeout'} 668 | cdict['Channel'] = channel 669 | cdict['Timeout'] = timeout 670 | response = self.send_action(cdict) 671 | return response 672 | 673 | def mailbox_count(self, mailbox): 674 | cdict = {'Action': 'MailboxCount'} 675 | cdict['Mailbox'] = mailbox 676 | response = self.send_action(cdict) 677 | return response 678 | 679 | def sippeers(self): 680 | cdict = {'Action': 'Sippeers'} 681 | response = self.send_action(cdict) 682 | return response 683 | 684 | def sipshowpeer(self, peer): 685 | cdict = {'Action': 'SIPshowpeer'} 686 | cdict['Peer'] = peer 687 | response = self.send_action(cdict) 688 | return response 689 | 690 | def sipshowregistry(self): 691 | cdict = {'Action': 'SIPShowregistry'} 692 | response = self.send_action(cdict) 693 | return response 694 | 695 | def iaxregistry(self): 696 | cdict = {'Action': 'IAXregistry'} 697 | response = self.send_action(cdict) 698 | return response 699 | 700 | def reload(self, module): 701 | """ Reloads config for a given module """ 702 | 703 | cdict = {'Action': 'Reload'} 704 | cdict['Module'] = module 705 | response = self.send_action(cdict) 706 | return response 707 | 708 | def dbdel(self, family, key): 709 | cdict = {'Action': 'DBDel'} 710 | cdict['Family'] = family 711 | cdict['Key'] = key 712 | response = self.send_action(cdict) 713 | return response 714 | 715 | def dbdeltree(self, family, key): 716 | cdict = {'Action': 'DBDelTree'} 717 | cdict['Family'] = family 718 | cdict['Key'] = key 719 | response = self.send_action(cdict) 720 | return response 721 | 722 | def dbget(self, family, key): 723 | cdict = {'Action': 'DBGet'} 724 | cdict['Family'] = family 725 | cdict['Key'] = key 726 | response = self.send_action(cdict) 727 | return response 728 | 729 | def dbput(self, family, key, val): 730 | cdict = {'Action': 'DBPut'} 731 | cdict['Family'] = family 732 | cdict['Key'] = key 733 | cdict['Val'] = val 734 | response = self.send_action(cdict) 735 | return response 736 | 737 | class ManagerException(Exception): 738 | pass 739 | 740 | 741 | class ManagerSocketException(ManagerException): 742 | pass 743 | 744 | 745 | class ManagerAuthException(ManagerException): 746 | pass 747 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = ../ 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyst2.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyst2.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyst2" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyst2" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/source/agi.rst: -------------------------------------------------------------------------------- 1 | AGI 2 | ========================= 3 | 4 | .. automodule:: asterisk.agi 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/agitb.rst: -------------------------------------------------------------------------------- 1 | AGITB 2 | ========================= 3 | 4 | .. automodule:: asterisk.agitb 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/changes.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ******* 3 | 4 | .. include:: ../../CHANGELOG 5 | 6 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyst2 documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Mar 7 10:41:33 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('../../')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.intersphinx', 34 | 'sphinx.ext.todo', 35 | 'sphinx.ext.coverage', 36 | 'sphinx.ext.ifconfig', 37 | 'sphinx.ext.viewcode', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = u'pyst2' 56 | copyright = u'2016, Randall Degges' 57 | author = u'Randall Degges' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = u'0.4.9' 65 | # The full version, including alpha/beta/rc tags. 66 | release = u'0.4.9' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = [] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | #keep_warnings = False 108 | 109 | # If true, `todo` and `todoList` produce output, else they produce nothing. 110 | todo_include_todos = True 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | html_theme = 'alabaster' 118 | 119 | # Theme options are theme-specific and customize the look and feel of a theme 120 | # further. For a list of options available for each theme, see the 121 | # documentation. 122 | #html_theme_options = {} 123 | 124 | # Add any paths that contain custom themes here, relative to this directory. 125 | #html_theme_path = [] 126 | 127 | # The name for this set of Sphinx documents. If None, it defaults to 128 | # " v documentation". 129 | #html_title = None 130 | 131 | # A shorter title for the navigation bar. Default is the same as html_title. 132 | #html_short_title = None 133 | 134 | # The name of an image file (relative to this directory) to place at the top 135 | # of the sidebar. 136 | #html_logo = None 137 | 138 | # The name of an image file (relative to this directory) to use as a favicon of 139 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 140 | # pixels large. 141 | #html_favicon = None 142 | 143 | # Add any paths that contain custom static files (such as style sheets) here, 144 | # relative to this directory. They are copied after the builtin static files, 145 | # so a file named "default.css" will overwrite the builtin "default.css". 146 | html_static_path = ['_static'] 147 | 148 | # Add any extra paths that contain custom files (such as robots.txt or 149 | # .htaccess) here, relative to this directory. These files are copied 150 | # directly to the root of the documentation. 151 | #html_extra_path = [] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | #html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | #html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | #html_sidebars = {} 163 | 164 | # Additional templates that should be rendered to pages, maps page names to 165 | # template names. 166 | #html_additional_pages = {} 167 | 168 | # If false, no module index is generated. 169 | #html_domain_indices = True 170 | 171 | # If false, no index is generated. 172 | #html_use_index = True 173 | 174 | # If true, the index is split into individual pages for each letter. 175 | #html_split_index = False 176 | 177 | # If true, links to the reST sources are added to the pages. 178 | #html_show_sourcelink = True 179 | 180 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 181 | #html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 184 | #html_show_copyright = True 185 | 186 | # If true, an OpenSearch description file will be output, and all pages will 187 | # contain a tag referring to it. The value of this option must be the 188 | # base URL from which the finished HTML is served. 189 | #html_use_opensearch = '' 190 | 191 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 192 | #html_file_suffix = None 193 | 194 | # Language to be used for generating the HTML full-text search index. 195 | # Sphinx supports the following languages: 196 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 197 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 198 | #html_search_language = 'en' 199 | 200 | # A dictionary with options for the search language support, empty by default. 201 | # Now only 'ja' uses this config value 202 | #html_search_options = {'type': 'default'} 203 | 204 | # The name of a javascript file (relative to the configuration directory) that 205 | # implements a search results scorer. If empty, the default will be used. 206 | #html_search_scorer = 'scorer.js' 207 | 208 | # Output file base name for HTML help builder. 209 | htmlhelp_basename = 'pyst2doc' 210 | 211 | # -- Options for LaTeX output --------------------------------------------- 212 | 213 | latex_elements = { 214 | # The paper size ('letterpaper' or 'a4paper'). 215 | #'papersize': 'letterpaper', 216 | 217 | # The font size ('10pt', '11pt' or '12pt'). 218 | #'pointsize': '10pt', 219 | 220 | # Additional stuff for the LaTeX preamble. 221 | #'preamble': '', 222 | 223 | # Latex figure (float) alignment 224 | #'figure_align': 'htbp', 225 | } 226 | 227 | # Grouping the document tree into LaTeX files. List of tuples 228 | # (source start file, target name, title, 229 | # author, documentclass [howto, manual, or own class]). 230 | latex_documents = [ 231 | (master_doc, 'pyst2.tex', u'pyst2 Documentation', 232 | u'Randall Degges', 'manual'), 233 | ] 234 | 235 | # The name of an image file (relative to this directory) to place at the top of 236 | # the title page. 237 | #latex_logo = None 238 | 239 | # For "manual" documents, if this is true, then toplevel headings are parts, 240 | # not chapters. 241 | #latex_use_parts = False 242 | 243 | # If true, show page references after internal links. 244 | #latex_show_pagerefs = False 245 | 246 | # If true, show URL addresses after external links. 247 | #latex_show_urls = False 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #latex_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #latex_domain_indices = True 254 | 255 | 256 | # -- Options for manual page output --------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [ 261 | (master_doc, 'pyst2', u'pyst2 Documentation', 262 | [author], 1) 263 | ] 264 | 265 | # If true, show URL addresses after external links. 266 | #man_show_urls = False 267 | 268 | 269 | # -- Options for Texinfo output ------------------------------------------- 270 | 271 | # Grouping the document tree into Texinfo files. List of tuples 272 | # (source start file, target name, title, author, 273 | # dir menu entry, description, category) 274 | texinfo_documents = [ 275 | (master_doc, 'pyst2', u'pyst2 Documentation', 276 | author, 'pyst2', 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | 292 | 293 | # Example configuration for intersphinx: refer to the Python standard library. 294 | intersphinx_mapping = {'https://docs.python.org/': None} 295 | -------------------------------------------------------------------------------- /docs/source/config.rst: -------------------------------------------------------------------------------- 1 | Config 2 | ========================= 3 | 4 | .. automodule:: asterisk.config 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/fastagi.rst: -------------------------------------------------------------------------------- 1 | fastAGI 2 | ========================= 3 | 4 | .. automodule:: asterisk.fastagi 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyst2 documentation master file, created by 2 | sphinx-quickstart on Mon Mar 7 10:41:33 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyst2's documentation! 7 | ================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | readme 15 | agi 16 | agitb 17 | config 18 | fastagi 19 | manager 20 | 21 | changes 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/source/manager.rst: -------------------------------------------------------------------------------- 1 | Manager 2 | ========================= 3 | 4 | .. automodule:: asterisk.manager 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | -------------------------------------------------------------------------------- /doctrees/agi.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/agi.doctree -------------------------------------------------------------------------------- /doctrees/agitb.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/agitb.doctree -------------------------------------------------------------------------------- /doctrees/changes.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/changes.doctree -------------------------------------------------------------------------------- /doctrees/config.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/config.doctree -------------------------------------------------------------------------------- /doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/environment.pickle -------------------------------------------------------------------------------- /doctrees/fastagi.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/fastagi.doctree -------------------------------------------------------------------------------- /doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/index.doctree -------------------------------------------------------------------------------- /doctrees/manager.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/manager.doctree -------------------------------------------------------------------------------- /doctrees/readme.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/doctrees/readme.doctree -------------------------------------------------------------------------------- /examples/agi_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Example to get and set variables via AGI. 5 | 6 | You can call directly this script with AGI() in Asterisk dialplan. 7 | """ 8 | 9 | from asterisk.agi import * 10 | 11 | agi = AGI() 12 | 13 | agi.verbose("python agi started") 14 | 15 | # Get variable environment 16 | extension = agi.env['agi_extension'] 17 | 18 | # Get variable in dialplan 19 | phone_exten = agi.get_variable('PHONE_EXTEN') 20 | 21 | # Set variable, it will be available in dialplan 22 | agi.set_variable('EXT_CALLERID', '1') -------------------------------------------------------------------------------- /examples/show_channels.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example to get list of active channels 3 | """ 4 | import asterisk.manager 5 | import sys 6 | 7 | manager = asterisk.manager.Manager() 8 | 9 | try: 10 | # connect to the manager 11 | try: 12 | manager.connect('localhost') 13 | manager.login('user', 'secret') 14 | 15 | # get a status report 16 | response = manager.status() 17 | print(response) 18 | 19 | response = manager.command('core show channels concise') 20 | print(response.data) 21 | 22 | manager.logoff() 23 | except asterisk.manager.ManagerSocketException as e: 24 | print "Error connecting to the manager: %s" % e.strerror 25 | sys.exit(1) 26 | except asterisk.manager.ManagerAuthException as e: 27 | print "Error logging in to the manager: %s" % e.strerror 28 | sys.exit(1) 29 | except asterisk.manager.ManagerException as e: 30 | print "Error: %s" % e.strerror 31 | sys.exit(1) 32 | 33 | finally: 34 | # remember to clean up 35 | manager.close() 36 | 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Packaging files and information.""" 2 | 3 | 4 | from setuptools import setup 5 | 6 | from asterisk import __version__ as version 7 | 8 | 9 | setup( 10 | 11 | # Basic package information: 12 | name = 'pyst2', 13 | version = version, 14 | packages = ['asterisk'], 15 | 16 | # Packaging options: 17 | zip_safe = False, 18 | include_package_data = True, 19 | 20 | # Package dependencies: 21 | install_requires = ['six>=1.9.0'], 22 | 23 | # Metadata for PyPI: 24 | author = 'Randall Degges', 25 | author_email = 'r@rdegges.com', 26 | license = 'Python Software Foundation License / GNU Library or Lesser General Public License (LGPL) / UNLICENSE', 27 | url = 'https://github.com/rdegges/pyst2', 28 | keywords = 'python asterisk agi ami telephony telephony sip voip', 29 | description = 'A Python Interface to Asterisk', 30 | long_description = open('README.rst').read(), 31 | 32 | # Classifiers: 33 | platforms = 'Any', 34 | classifiers = [ 35 | 'Development Status :: 5 - Production/Stable', 36 | 'Environment :: Other Environment', 37 | 'Intended Audience :: Developers', 38 | 'Intended Audience :: Telecommunications Industry', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 2.6', 42 | 'Programming Language :: Python :: 2.7', 43 | 'Programming Language :: Python :: 3', 44 | 'Programming Language :: Python :: 3.2', 45 | 'Programming Language :: Python :: 3.3', 46 | 'Programming Language :: Python :: 3.4', 47 | 'Topic :: Communications :: Internet Phone', 48 | 'Topic :: Communications :: Telephony', 49 | 'Topic :: Software Development :: Libraries :: Python Modules' 50 | ], 51 | 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfernandz/pyst2/ee2220bb7813ec19ff3bcb2afd9115286e0e30af/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from asterisk.manager import Manager 3 | from itertools import combinations 4 | 5 | 6 | class TestBasic(TestCase): 7 | def test_action_id(self): 8 | """ ensure that no two actionIDs are the same, even if they come 9 | from separate manager instances. Otherwise you risk aliasing 10 | events if you, for example, subscribe to all events while 11 | originating two different phone calls in different processes. """ 12 | 13 | manager1 = Manager() 14 | manager2 = Manager() 15 | actionIDs = [ 16 | manager1.get_actionID(), 17 | manager1.get_actionID(), 18 | manager2.get_actionID(), 19 | manager2.get_actionID(), 20 | ] 21 | for a, b in combinations(actionIDs, 2): 22 | self.assertNotEqual(a, b) 23 | --------------------------------------------------------------------------------