class ChannelTracker(propertied.Propertied): """Track open channels on the Asterisk server""" channels = common.DictionaryProperty( "channels", """Set of open channels on the system""", ) thresholdCount = common.IntegerProperty( "thresholdCount", """Storage of threshold below which we don't warn user""", defaultValue=20, ) def main(self): """Main operation for the channel-tracking demo""" APPLICATION.amiSpecifier.login().addCallback(self.onAMIConnect) def onAMIConnect(self, ami): ami.status().addCallback(self.onStatus, ami=ami) ami.registerEvent('Hangup', self.onChannelHangup) ami.registerEvent('Newchannel', self.onChannelNew) def onStatus(self, events, ami=None): """Integrate the current status into our set of channels""" log.debug("""Initial channel status retrieved""") for event in events: self.onChannelNew(ami, event) def onChannelNew(self, ami, event): """Handle creation of a new channel""" log.debug("""Start on channel %s""", event) opening = event['uniqueid'] not in self.channels self.channels[event['uniqueid']] = event if opening: self.onChannelChange(ami, event, opening=opening) def onChannelHangup(self, ami, event): """Handle hangup of an existing channel""" try: del self.channels[event['uniqueid']] except KeyError as err: log.warn("""Hangup on unknown channel %s""", event) else: log.debug("""Hangup on channel %s""", event) self.onChannelChange(ami, event, opening=False) def onChannelChange(self, ami, event, opening=False): """Channel count has changed, do something useful like enforcing limits""" if opening and len(self.channels) > self.thresholdCount: log.warn("""Current channel count: %s""", len(self.channels)) else: log.info("""Current channel count: %s""", len(self.channels))
class Tag(propertied.Propertied): """Represents a particular tag within a document""" name = common.StringProperty( "name", "The name of the tag", defaultValue="", ) attributes = common.DictionaryProperty( "attributes", """The in-tag attributes of the tag""", defaultFunction=lambda x, y: {}, ) content = common.ListProperty( "content", """The content (children) of the tag""", setDefaultOnGet=1, defaultFunction=lambda x, y: [], ) def __cmp__(self, other): """Compare this tag to another""" if not isinstance(other, Tag): return -1 if other.name != self.name: return cmp(self.name, other.name) if other.attributes != self.attributes: return cmp(self.attributes, other.attributes) if other.content != self.content: return cmp(self.content, other.content) return 0 def __repr__(self): """Create a decent representation of this tag""" fragments = [] name = self.name.decode().encode('utf-8') fragments.append("<" + name) for key, value in self.attributes.items(): fragments.append("""%s=%r""" % (key, value)) fragments = [" ".join(fragments)] if self.content: fragments.append(">") for item in self.content: if isinstance(item, str): fragments.append(item) else: fragments.append(repr(item)) fragments.append("</%s>" % (name)) else: fragments.append("/>") return "".join(fragments)
class WithProps(object): currentState = common.IntegerProperty( "currentState", """The current state of this instance""", defaultValue=3, ) someSource = common.DictionaryProperty( "someSource", """Source for properties in state 4""", ) someProp = StateBasedProperty( "someProp", """A state-aware generic property""", defaultFunction=lambda prop, client: client.__class__.__name__, setDefaultOnGet=0, ) someOtherProp = DictStateBasedProperty( "someOtherProp", """A state-aware dictionary property (with automatic default""", )
class Simple(propertied.Propertied): count = common.IntegerProperty( "count", """Count some value for us""", defaultValue=0, ) names = common.StringsProperty( "names", """Some names as a list of strings""", ) mapping = common.DictionaryProperty( "mapping", """Mapping from name to number""", defaultValue=[ ('tim', 3), ('tom', 4), ('bryan', 5), ], ) def __repr__(self): className = self.__class__.__name__ def clean(value): value = value.splitlines()[0] if len(value) > 30: value = value[:27] + '...' return value props = ", ".join([ '%s=%s' % (prop.name, repr(prop.__get__(self))) for prop in self.getProperties() if hasattr(self, prop.name) ]) return '<%(className)s %(props)s>' % locals() __str__ = __repr__
class Application(utilapplication.UtilApplication): """Services provided at the application level""" surveys = common.DictionaryProperty( "surveys", """Set of surveys indexed by survey/extension number""", )
class UtilApplication(propertied.Propertied): """Utility class providing simple application-level operations FastAGI entry points are waitForCallOn and handleCallsFor, which allow for one-shot and permanant handling of calls for an extension (respectively), and agiSpecifier, which is loaded from configuration file (as specified in self.configFiles). """ amiSpecifier = basic.BasicProperty( "amiSpecifier", """AMI connection specifier for the application see AMISpecifier""", defaultFunction=lambda prop, client: AMISpecifier()) agiSpecifier = basic.BasicProperty( "agiSpecifier", """FastAGI server specifier for the application see AGISpecifier""", defaultFunction=lambda prop, client: AGISpecifier()) extensionWaiters = common.DictionaryProperty( "extensionWaiters", """Set of deferreds waiting for incoming extensions""", ) extensionHandlers = common.DictionaryProperty( "extensionHandlers", """Set of permanant callbacks waiting for incoming extensions""", ) configFiles = configFiles = ('starpy.conf', '~/.starpy.conf') def __init__(self): """Initialise the application from options in configFile""" self.loadConfigurations() def loadConfigurations(self): parser = self._loadConfigFiles(self.configFiles) self._copyPropertiesFrom(parser, 'AMI', self.amiSpecifier) self._copyPropertiesFrom(parser, 'FastAGI', self.agiSpecifier) return parser def _loadConfigFiles(self, configFiles): """Load options from configuration files given (if present)""" parser = ConfigParser() filenames = [ os.path.abspath(os.path.expandvars(os.path.expanduser(file))) for file in configFiles ] log.info("Possible configuration files:\n\t%s", "\n\t".join(filenames) or None) filenames = [file for file in filenames if os.path.isfile(file)] log.info("Actual configuration files:\n\t%s", "\n\t".join(filenames) or None) parser.read(filenames) return parser def _copyPropertiesFrom(self, parser, section, client, properties=None): """Copy properties from the config-parser's given section into client""" if properties is None: properties = client.getProperties() for property in properties: if parser.has_option(section, property.name): try: value = parser.get(section, property.name) setattr(client, property.name, value) except (TypeError, ValueError, AttributeError, NameError), err: log("""Unable to set property %r of %r to config-file value %r: %s""" % ( property.name, client, parser.get(section, property.name, 1), err, )) return client
class ChannelTracker(propertied.Propertied): """Track open channels on the Asterisk server""" channels = common.DictionaryProperty( "channels", """Set of open channels on the system""", ) thresholdCount = common.IntegerProperty( "thresholdCount", """Storage of threshold below which we don't warn user""", defaultValue=20, ) def main(self): """Main operation for the channel-tracking demo""" amiDF = APPLICATION.amiSpecifier.login().addCallback(self.onAMIConnect) # XXX do something useful on failure to login... def onAMIConnect(self, ami): """Register for AMI events""" # XXX should do an initial query to populate channels... # XXX should handle asterisk reboots (at the moment the AMI # interface will just stop generating events), not a practical # problem at the moment, but should have a periodic check to be sure # the interface is still up, and if not, should close and restart log.debug('onAMIConnect') ami.status().addCallback(self.onStatus, ami=ami) ami.registerEvent('Hangup', self.onChannelHangup) ami.registerEvent('Newchannel', self.onChannelNew) def interestingEvent(self, event, ami=None): """Decide whether this channel event is interesting Real-world application would want to take only Zap channels, or only channels from a given context, or whatever other filter you want in order to capture *just* the scarce resource (such as PRI lines). Keep in mind that an "interesting" event must show up as interesting for *both* Newchannel and Hangup events or you will leak references/channels or have unknown channels hanging up. """ return True def onStatus(self, events, ami=None): """Integrate the current status into our set of channels""" log.debug("""Initial channel status retrieved""") for event in events: self.onChannelNew(ami, event) def onChannelNew(self, ami, event): """Handle creation of a new channel""" log.debug("""Start on channel %s""", event) if self.interestingEvent(event, ami): opening = not self.channels.has_key(event['uniqueid']) self.channels[event['uniqueid']] = event if opening: self.onChannelChange(ami, event, opening=opening) def onChannelHangup(self, ami, event): """Handle hangup of an existing channel""" if self.interestingEvent(event, ami): try: del self.channels[event['uniqueid']] except KeyError, err: log.warn("""Hangup on unknown channel %s""", event) else: log.debug("""Hangup on channel %s""", event) self.onChannelChange(ami, event, opening=False)
class UtilApplication(propertied.Propertied): """Utility class providing simple application-level operations FastAGI entry points are waitForCallOn and handleCallsFor, which allow for one-shot and permanant handling of calls for an extension (respectively), and agiSpecifier, which is loaded from configuration file (as specified in self.configFiles). """ amiSpecifier = basic.BasicProperty( "amiSpecifier", """AMI connection specifier for the application see AMISpecifier""", defaultFunction=lambda prop, client: AMISpecifier()) agiSpecifier = basic.BasicProperty( "agiSpecifier", """FastAGI server specifier for the application see AGISpecifier""", defaultFunction=lambda prop, client: AGISpecifier()) extensionWaiters = common.DictionaryProperty( "extensionWaiters", """Set of deferreds waiting for incoming extensions""", ) extensionHandlers = common.DictionaryProperty( "extensionHandlers", """Set of permanant callbacks waiting for incoming extensions""", ) configFiles = ('starpy.conf', '~/.starpy.conf') def __init__(self): """Initialise the application from options in configFile""" self.loadConfigurations() def loadConfigurations(self): parser = self._loadConfigFiles(self.configFiles) self._copyPropertiesFrom(parser, 'AMI', self.amiSpecifier) self._copyPropertiesFrom(parser, 'FastAGI', self.agiSpecifier) return parser def _loadConfigFiles(self, configFiles): """Load options from configuration files given (if present)""" parser = ConfigParser() filenames = [ os.path.abspath(os.path.expandvars(os.path.expanduser(file))) for file in configFiles ] log.info("Possible configuration files:\n\t%s", "\n\t".join(filenames) or None) filenames = [file for file in filenames if os.path.isfile(file)] log.info("Actual configuration files:\n\t%s", "\n\t".join(filenames) or None) parser.read(filenames) return parser def _copyPropertiesFrom(self, parser, section, client, properties=None): """Copy properties from the config-parser's given section into client""" if properties is None: properties = client.getProperties() for property in properties: if parser.has_option(section, property.name): try: value = parser.get(section, property.name) setattr(client, property.name, value) except (TypeError, ValueError, AttributeError, NameError) as err: log('Unable to set property %r of %r to config-file value %r: %s' % ( property.name, client, parser.get(section, property.name, 1), err, )) return client def dispatchIncomingCall(self, agi): """Handle an incoming call (dispatch to the appropriate registered handler)""" extension = agi.variables['agi_extension'] log.info("""AGI connection with extension: %r""", extension) try: df = self.extensionWaiters.pop(extension) except KeyError as err: try: callback = self.extensionHandlers[extension] except KeyError as err: try: callback = self.extensionHandlers[None] except KeyError as err: log.warn("""Unexpected connection to extension %r: %s""", extension, agi.variables) agi.finish() return try: return callback(agi) except Exception as err: log.error("""Failure during callback %s for agi %s: %s""", callback, agi.variables, err) # XXX return a -1 here else: if not df.called: df.callback(agi) def waitForCallOn(self, extension, timeout=15): """Wait for an AGI call on extension given extension -- string extension for which to wait timeout -- duration in seconds to wait before defer.TimeoutError is returned to the deferred. Note that waiting callback overrides any registered handler; that is, if you register one callback with waitForCallOn and another with handleCallsFor, the first incoming call will trigger the waitForCallOn handler. returns deferred returning connected FastAGIProtocol or an error """ extension = str(extension) log.info('Waiting for extension %r for %s seconds', extension, timeout) df = defer.Deferred() self.extensionWaiters[extension] = df def onTimeout(): if not df.called: df.errback( defer.TimeoutError( """Timeout waiting for call on extension: %r""" % (extension, ))) reactor.callLater(timeout, onTimeout) return df def handleCallsFor(self, extension, callback): """Register permanant handler for given extension extension -- string extension for which to wait or None to define a default handler (that chosen if there is not explicit handler or waiter) callback -- callback function to be called for each incoming channel to the given extension. Note that waiting callback overrides any registered handler; that is, if you register one callback with waitForCallOn and another with handleCallsFor, the first incoming call will trigger the waitForCallOn handler. returns None """ if extension is not None: extension = str(extension) self.extensionHandlers[extension] = callback