class Interaction(propertied.Propertied): """Base class for user-interaction operations""" ALL_DIGITS = '0123456789*#' timeout = common.FloatProperty( "timeout", """Duration to wait for response before repeating message""", defaultValue=5, ) maxRepetitions = common.IntegerProperty( "maxRepetitions", """Maximum number of times to play before failure""", defaultValue=5, ) onSuccess = basic.BasicProperty( "onSuccess", """Optional callback for success with signature method( result, runner )""", ) onFailure = basic.BasicProperty( "onFailure", """Optional callback for failure with signature method( result, runner )""", ) runnerClass = None def __call__(self, agi, *args, **named): """Initiate AGI-based interaction with the user""" return self.runnerClass(model=self, agi=agi)(*args, **named)
class Contact( ContactObject ): names = basic.BasicProperty( "names", "List of names for the contact", boundaries = [ boundary.ForEach( boundary.Type( Name )) ], defaultFunction = lambda x,y: [], ) phones = basic.BasicProperty( "phones", "List of phone numbers for the contact", boundaries = [ boundary.ForEach( boundary.Type( PhoneNumber )) ], defaultFunction = lambda x,y: [], ) emails = basic.BasicProperty( "emails", "List of email addresses for the contact", boundaries = [ boundary.ForEach( boundary.Type( EmailAddress )) ], defaultFunction = lambda x,y: [], ) deliveries = basic.BasicProperty( "deliveries", "List of delivery addresses for the contact", boundaries = [ boundary.ForEach( boundary.Type( DeliveryAddress )) ], defaultFunction = lambda x,y: [], )
class TestClass(object): col = wxtypes.ColourProperty( "col", "Colour property", ) pen = wxtypes.PenProperty( "pen", "Pen property", ) penStyle = basic.BasicProperty( "penStyle", "Pen style property", baseType=pen_module.PenStyle, ) penCap = basic.BasicProperty( "penCap", "Pen cap property", baseType=pen_module.PenCap, ) penJoin = basic.BasicProperty( "penJoin", "Pen Join property", baseType=pen_module.PenJoin, )
class TestClass(object): arg = basic.BasicProperty( "arg", "Single Argument", baseType=callable.Argument, ) args = basic.BasicProperty( "args", "Multiple Arguments", baseType=callable.listof_Arguments, ) call = basic.BasicProperty( 'call', 'single callable', baseType=callable.Callable, )
class Prompt(propertied.Propertied): """A Prompt to be read to the user""" value = basic.BasicProperty( "value", """Filename to be read to the user""", ) def __init__(self, value, **named): named['value'] = value super(Prompt, self).__init__(**named)
class DateTimePrompt(Prompt): """Prompt that reads a date/time as a date""" format = basic.BasicProperty( "format", """Format in which to read the date to the user""", defaultValue = None ) def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayDateTime(self.value, escapeDigits, format=self.format)
def testRepr(self): """Test representation code with real objects""" property = basic.BasicProperty("this") bound = boundary.Type(str) client = self value = "some value" object = boundary.BoundaryError(property, bound, client, value, "somemessage") repr(object)
def testStr(self): """Test string-conversion code with real objects""" property = basic.BasicProperty("this") bound = boundary.Type(str) client = self value = "some value" object = boundary.BoundaryError(property, bound, client, value, "somemessage") str(object)
class TestClass(object): simple = basic.BasicProperty("simple", "documentation") withBound = basic.BasicProperty("withBound", "documentation", boundaries=(boundary.Type(str), )) withDefaultValue = basic.BasicProperty( "withDefaultValue", "documentation", defaultValue='this', ) withDefaultFunction = basic.BasicProperty( "withDefaultFunction", "documentation", defaultFunction=lambda x, y: [], ) withDefaultValueNoSet = basic.BasicProperty( "withDefaultValueNoSet", "documentation", defaultValue='this', setDefaultOnGet=0, ) withDefaultFunctionNoSet = basic.BasicProperty( "withDefaultFunctionNoSet", "documentation", defaultFunction=lambda x, y: [], setDefaultOnGet=0, )
class EnumerationChoice(propertied.Propertied): """A particular choice within an enumeration set The enumeration choice is a particular choice stored within the enumeration set. Its name is used to index the choice within its set, while its value is the actual value being enumerated. """ name = basic.BasicProperty( "name", """The internal name/key used to identify the choice""", baseType=basic_types.String_DT, ) value = basic.BasicProperty( "value", """The data value associated with this choice""", ) friendlyName = basic.BasicProperty( "friendlyName", """Friendly name used to describe this choice to users""", setDefaultOnGet=0, defaultFunction=defaultFriendly, baseType=basic_types.String_DT, ) def __repr__(self): """Get a code-like representation of this choice""" if self.friendlyName != self.name: return """%s( name=%r, value=%r, friendlyName=%r)""" % ( self.__class__.__name__, self.name, self.value, self.friendlyName, ) else: return """%s( name=%r, value=%r)""" % ( self.__class__.__name__, self.name, self.value, )
class ContactObject( propertied.Propertied, Persistence.Persistent): """A basic object within the contact database Each object within the database supports some number of note fields """ notes = basic.BasicProperty( "notes", "Notes about the given value", boundaries = [ boundary.Type( list ), boundary.ForEach( boundary.Type( unicode )), ], defaultFunction = lambda x,y: [], )
def _testError(self, bound, badValue): """Test that the error object is properly configured""" property = basic.BasicProperty("this") client = self try: bound(badValue, property, client) except boundary.BoundaryError, error: assert error.property is property, """Improper error attribute %s""" % ( property, ) assert error.boundary is bound, """Improper error attribute %s""" % ( bound, ) assert error.client is client, """Improper error attribute %s""" % ( client, ) assert error.value is badValue, """Improper error attribute %s""" % ( badValue, )
def testInit(self): """Test initialisation of the property objects""" basic.BasicProperty("name") basic.BasicProperty("name", "documentation") basic.BasicProperty( "name", "documentation", ) basic.BasicProperty("name", "documentation", defaultValue=[1, 2, 3]) basic.BasicProperty("name", "documentation", defaultFunction=lambda x, y: []) basic.BasicProperty("name", "documentation", baseType=str)
class SubMenu(Option): """A menu-holding option, just forwards call to the held menu""" menu = basic.BasicProperty( "menu", """The sub-menu we are presenting to the user""", ) def __call__(self, pressed, parent): """Get result from the sub-menu, add ourselves into the result""" def onResult(result): log.debug("""Child menu %s result: %s""", self.menu, result) result.insert(0, (self,pressed)) return result def onFailure(reason): """Trap voluntary exit and re-start the parent menu""" reason.trap(error.MenuExit) log.warn("""Restarting parent menu: %s""", parent) return parent.model(parent.agi) return self.menu(parent.agi).addCallbacks(onResult, onFailure)
class PromptRunner(propertied.Propertied): """Prompt formed from list of sub-prompts """ elements = common.ListProperty( "elements", """Sub-elements of the prompt to be presented""", ) agi = basic.BasicProperty( "agi", """The FastAGI instance we're controlling""", ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from playing the prompt""", ) timeout = common.FloatProperty( "timeout", """Timeout on data-entry after completed reading""", ) def __call__(self): """Return a deferred that chains all of the sub-prompts in order Returns from the first of the sub-prompts that recevies a selection returns str(digit) for the key the user pressed """ return self.onNext(None) def onNext(self, result, index=0): """Process the next operation""" if result is not None: return result try: element = self.elements[index] except IndexError, err: # okay, do a waitForDigit from timeout seconds... return self.agi.waitForDigit(self.timeout).addCallback( self.processKey).addCallback(self.processLast) else:
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
class Survey(propertied.Propertied): """Models a single survey to be completed""" surveyId = common.IntegerProperty( "surveyId", """Unique identifier for this survey""", ) owner = basic.BasicProperty( "owner", """Owner's phone number to which to connect""", ) questions = common.ListProperty( "questions", """Set of questions which make up the survey""", ) YOU_CURRENTLY_HAVE = 'vm-youhave' QUESTIONS_IN_YOUR_SURVEY = 'vm-messages' QUESTION_IN_YOUR_SURVEY = 'vm-message' TO_LISTEN_TO_SURVEY_QUESTION = 'to-listen-to-it' TO_RECORD_A_NEW_SURVEY_QUESTION = 'to-rerecord-it' TO_FINISH_SURVEY_SETUP = 'vm-helpexit' def setupSurvey(self, agi): """AGI application to allow the user to set up the survey Screen 1: You have # questions. To listen to a question, press the number of the question. To record a new question, press pound. To finish setup, press star. """ seq = fastagi.InSequence() seq.append(agi.wait, 2) base = """You currently have %s question%s. To listen to a question press the number of the question. To record a new question, press pound. To finish survey setup, press star. """ % ( len(self.questions), ['', 's'][len(self.questions) == 1], ) if len(base) != 1: base += 's' base = " ".join(base.split()) seq.append(agi.execute, 'Festival', base) seq.append(agi.finish, ) return seq() seq.append(agi.streamFile, self.YOU_CURRENTLY_HAVE) seq.append(agi.sayNumber, len(self.questions)) if len(self.questions) == 1: seq.append(agi.streamFile, self.QUESTION_IN_YOUR_SURVEY) else: seq.append(agi.streamFile, self.QUESTIONS_IN_YOUR_SURVEY) seq.append(agi.streamFile, self.TO_LISTEN_TO_SURVEY_QUESTION) seq.append(agi.streamFile, self.TO_RECORD_A_NEW_SURVEY_QUESTION) seq.append(agi.streamFile, self.TO_FINISH_SURVEY_SETUP) seq.append(agi.finish, ) return seq() def newQuestionId(self): """Return a new, unique, question id""" bad = True while bad: bad = False id = random.randint(0, sys.maxint) for question in self.questions: if id == question.__dict__.get('questionId'): bad = True return id
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
defaultFunction = lambda x,y: [], ) deliveries = basic.BasicProperty( "deliveries", "List of delivery addresses for the contact", boundaries = [ boundary.ForEach( boundary.Type( DeliveryAddress )) ], defaultFunction = lambda x,y: [], ) class OrganisationMember( ContactObject ): title = common.StringProperty( "title", "The title of this position", defaultValue = "", ) _members = basic.BasicProperty( "members", "The members/contacts in the position", boundaries = [ boundary.ForEach( boundary.Type( (Contact, OrganisationMember) )) ], defaultFunction = lambda x,y: [], ) OrganisationMember.members = _members class Organisation( Contact ): members = _members
class Enumeration(propertied.Propertied): """A choice from an enumerated set of data values This class also operates as the base-type for the enumeration properties, via the data-type-definition API. """ dataType = "enumeration" ## set must be class-data, not just instance data ## should probably be a metaclass property of EnumerationSet type set = None name = basic.BasicProperty( "name", """Data-value choice within one of our sets""", defaultValue="", baseType=unicode, ) def __init__(self, name="", *arguments, **named): if not isinstance(name, (str, unicode)): name = self.__class__.set.getName(name) super(Enumeration, self).__init__(name=name, *arguments, **named) if not self.choice(): raise ValueError("""Name %r is not part of %s""" % (self.name, self.__class__.__name__)) def choice(self): """Get the choice object associated with this value or None""" return self.set.get(self.name) def value(self): """Get the value associated with this choice""" choice = self.choice() if choice is not None: return choice.value raise ValueError("""Could not get value for name %r for %s""" % (self.name, self.__class__.__name__)) def __cmp__(self, other): """Compare this value to another value""" if isinstance(other, Enumeration): return cmp(self.value(), other.value()) else: return cmp(self.value(), other) def __repr__(self): """Return a code-like representation of this object""" return """%s( name=%r)""" % (self.__class__.__name__, self.name) def __str__(self): """Return the enumeration value as a name""" return self.name or self.value() ### Data-type-definition API def check(cls, value): """Check whether value is of cls type, and has the same set""" return isinstance(value, cls) and cls.set == value.set check = classmethod(check) def coerce(cls, value): """Coerce a value into an Enumeration value Accepted types: Enumeration objects integers/longs ([name,name,name],remainder) tuples [name,name,name,value] lists (values are |'d together) """ if cls.check(value): return value elif isinstance(value, (str, unicode)): return cls.parse(value) else: return cls.fromValue(value) coerce = classmethod(coerce) def fromValue(cls, value): """Create from an integer value""" name = cls.set.getName(value) if name is None: raise ValueError("""Value %r is not part of %s""" % (value, cls.__name__)) else: return cls(name=name) fromValue = classmethod(fromValue) def parse(cls, value): """Create from a string value Possible formats: "coreName" "23" "friendlyName" """ value = value.strip() current = cls.set.get(value) if current is not None: return cls(name=value) else: return cls.fromValue(value) parse = classmethod(parse) def allInstances(cls): """Return cls instances for each of this class's set""" items = [(choice.friendlyName, cls(name=choice.name)) for choice in cls.set.values()] items.sort() items = [v[1] for v in items] return items allInstances = classmethod(allInstances)
class Runner(propertied.Propertied): """User's interaction with a given Interaction-type""" agi = basic.BasicProperty( "agi", """The AGI instance we use to communicate with the user""", ) def defaultFinalDF(prop, client): """Produce the default finalDF with onSuccess/onFailure support""" df = defer.Deferred() model = client.model if hasattr(model, 'onSuccess'): log.debug('register onSuccess: %s', model.onSuccess) df.addCallback(model.onSuccess, runner=client) if hasattr(model, 'onFailure'): log.debug('register onFailure: %s', model.onFailure) df.addErrback(model.onFailure, runner=client) return df finalDF = basic.BasicProperty( "finalDF", """Final deferred we will callback/errback on success/failure""", defaultFunction = defaultFinalDF, ) del defaultFinalDF alreadyRepeated = common.IntegerProperty( "alreadyRepeated", """Number of times we've repeated the message...""", defaultValue = 0, ) model = basic.BasicProperty( "model", """The data-model that we are presenting to the user (e.g. Menu)""", ) def returnResult(self, result): """Return result of deferred to our original caller""" log.debug('returnResult: %s %s', self.model,result) if not self.finalDF.called: self.finalDF.debug = True self.finalDF.callback(result) else: log.debug('finalDF already called, ignoring %s', result) return result def returnError(self, reason): """Return failure of deferred to our original caller""" log.debug('returnError: %s', self.model) if not isinstance(reason.value, error.MenuExit): log.warn("""Failure during menu: %s""", reason.getTraceback()) if not self.finalDF.called: self.finalDF.debug = True self.finalDF.errback(reason) else: log.debug('finalDF already called, ignoring %s', reason.getTraceback()) def promptAsRunner(self, prompt): """Take set of prompt-compatible objects and produce a PromptRunner for them""" realPrompt = [] for p in prompt: if isinstance(p, (str, unicode)): p = AudioPrompt(p) elif isinstance(p, int): p = NumberPrompt(p) elif not isinstance(p, Prompt): raise TypeError( """Unknown prompt element type on %r: %s"""%( p, p.__class__, )) realPrompt.append(p) return PromptRunner( elements = realPrompt, escapeDigits = self.escapeDigits, agi = self.agi, timeout = self.model.timeout, )
class PTest(propertied.Propertied): value = basic.BasicProperty( "value", """Testing value""", baseType=Test2, )
class Argument(propertied.Propertied): """Representation of a single argument on a callable object""" name = common.StringLocaleProperty( 'name', """The argument's name, as a simple string""", ) default = basic.BasicProperty( 'default', """Default-value for the argument, may be NULL/unavailable""", ) baseType = basic.BasicProperty( 'baseType', """Base data-type for the argument, may be NULL/unavailable""", ) def __init__(self, name, default=__NULL__, baseType=__NULL__, **named): """Initialize the Callable object name -- the argument name default -- if provided, will provide the default value for the argument baseType -- if provided, will allow for type checking and coercion of arguments before calling the callable object. """ if default is not __NULL__: named["default"] = default if baseType is not __NULL__: named["baseType"] = baseType super(Argument, self).__init__(name=name, **named) def __str__(self, ): """Create a friendly string representation""" fragments = [repr(self.name)] if hasattr(self, "default"): fragments.append(repr(self.default)) if hasattr(self, "baseType"): fragments.append(repr(self.baseType)) return """%s(%s)""" % ( self.__class__.__name__, ", ".join(fragments), ) __repr__ = __str__ def __eq__(self, other): """Determine whether other is our equivalent returns true if other is of the same class, with the same primary attributes """ if self.__class__ is not other.__class__: return 0 NULL = [] for nm in ['name', 'default', 'baseType']: if hasattr(self, nm) and not hasattr(other, nm): return 0 elif not hasattr(self, nm) and hasattr(other, nm): return 0 elif hasattr(self, nm): if getattr(self, nm) != getattr(other, nm): return 0 return 1 ### Data-type API def check(cls, value): """Strict check to see if value is an instance of cls""" return isinstance(value, cls) check = classmethod(check) def coerce(cls, value): """Coerce value to a cls instance Accepted forms: ("name",) ("name",default) ("name",default,baseType) "name" { ** } # passed to the initialiser """ if cls.check(value): return value if isinstance(value, (tuple, list)) and value and len(value) < 4: items = {} for item, name in zip(value, ['name', 'default', 'baseType'][:len(value)]): items[name] = item return cls(**items) elif isinstance(value, str): return cls(name=value) elif isinstance(value, dict): return cls(**value) raise TypeError("""Don't know how to convert %r to a %s object""" % (value, cls.__name__)) coerce = classmethod(coerce)
class Callable(propertied.Propertied): """Modelling of a callable Python object""" name = common.StringProperty( 'name', """The callable object's-name (may be different from underlying object)""", ) implementation = basic.BasicProperty( "implementation", """The underlying implementation (callable Python object)""", ) arguments = common.ListProperty( 'arguments', """Argument-list for the callable object""", baseType=listof_Arguments, ) shortHelp = common.StringProperty( 'shortHelp', """Short help-string suitable for tooltips/status-bars""", ) longHelp = common.StringProperty( 'longHelp', """Longer help-string suitable for context-sensitive help""", ) coerce = common.BooleanProperty( "coerce", """Whether to coerce arguments if possible""", defaultValue=0, ) def __init__(self, implementation, name=__NULL__, arguments=__NULL__, shortHelp=__NULL__, longHelp=__NULL__, **named): """Initialize the Callable object implementation -- a callable python object name -- if provided, will override the given name arguments -- if provided, will override calculated arguments shortHelp -- short help-string, first line of __doc__ if not given longHelp -- long help-string, entire __doc__ string if not given """ if name is __NULL__: name = self._name(implementation) if arguments is __NULL__: arguments = self._arguments(implementation) if shortHelp is __NULL__: shortHelp = self._shortHelp(implementation) if longHelp is __NULL__: longHelp = self._longHelp(implementation) super(Callable, self).__init__(implementation=implementation, name=name, arguments=arguments, **named) def __str__(self): """Return a friendly string representation""" return """%s( %s )""" % (self.__class__.__name__, self.implementation) def __call__(self, *arguments, **named): """Do the actual calling of the callable object""" set = {} for argument, value in zip(arguments, self.arguments): set[argument.name] = (argument, value) # XXX potentially there are missing positional arguments! if named: nameSet = dict([(arg.name, arg) for arg in self.arguments]) for key, value in named.items(): if set.has_key(key): raise ValueError( """Redefinition of argument order for argument %s""" % (set.get(key))) else: # note that argument may be None set[key] = nameSet.get(key), value for key, (argument, value) in set.items(): if self.coerce and argument and argument.baseType and hasattr( argument.baseType, "coerce"): value = argument.baseType.coerce(argument) set[key] = value # XXX Should keep arguments in order to allow for *args set :( return self.implementation(**set) def getArgument(self, name): """Retieve an argument by name""" for argument in self.arguments: if argument.name == name: return argument raise KeyError("""%r object doesn't have a %s argument""" % (self, name)) def _name(self, value): """Try to find a decent name for a callable object""" name = "<unknown>" for attribute in [ '__name__', 'name', 'func_name', 'co_name', '__file__', "friendlyName" ]: if hasattr(value, attribute): v = getattr(value, attribute) if isinstance(v, (str, unicode)): name = v if '.' in name: return name.split('.')[-1] return name def _shortHelp(self, value): """Try to find the short-docstring for an object""" if hasattr(value, '__doc__') and value.__doc__: return value.__doc__.split('\n')[0] else: return "" def _longHelp(self, value): """Try to find the short-docstring for an object""" if hasattr(value, '__doc__') and value.__doc__: return value.__doc__ else: return "" def _useCall(self, value): """Can we use __call__ to call this object? returns true if we should be able to use it """ return ( # must have __call__ hasattr(value, '__call__') and ( # call should be a function or method... hasattr(value.__call__, 'im_func') or hasattr(value.__call__, 'im_code'))) def _arguments(self, value): """Get a list of arguments for a callable object""" if self._useCall(value): value = value.__call__ if hasattr(value, 'im_func'): # receiver is a method. Drop the first argument, usually 'self'. func = value.im_func arguments = inspect.getargspec(func) if value.im_self is not None: # a bound instance or class method arguments = inspect.getargspec(func) del arguments[0][0] else: # an un-bound method pass elif hasattr(value, 'func_code') or hasattr(value, 'im_code'): # receiver is a function. func = value arguments = inspect.getargspec(func) else: raise ValueError('unknown reciever type %s %s' % (receiver, type(receiver))) names, vararg, varnamed, defaults = arguments defaults = defaults or () result = [Argument(name=name) for name in names] for name, default in zip(names[-len(defaults):], defaults): for item in result: if item.name == name: item.default = default return result def check(cls, value): """Strict check to see if value is an instance of cls""" return isinstance(value, cls) check = classmethod(check) def coerce(cls, value): """Coerce value to a Callable-object""" if cls.check(value): return value if callable(value): return cls(implementation=value, ) else: raise TypeError("Don't know how to convert %r to a %s object" % ( value, cls.__name__, )) coerce = classmethod(coerce) def __eq__(self, other): """Determine whether other is our equivalent returns true if other is of the same class, with the same primary attributes """ if self.__class__ is not other.__class__: return 0 NULL = [] for nm in ['name', 'implementation', 'arguments']: if hasattr(self, nm) and not hasattr(other, nm): return 0 elif not hasattr(self, nm) and hasattr(other, nm): return 0 elif hasattr(self, nm): if getattr(self, nm) != getattr(other, nm): return 0 return 1
class PromptRunner(propertied.Propertied): """Prompt formed from list of sub-prompts """ elements = common.ListProperty( "elements", """Sub-elements of the prompt to be presented""", ) agi = basic.BasicProperty( "agi", """The FastAGI instance we're controlling""", ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from playing the prompt""", ) timeout = common.FloatProperty( "timeout", """Timeout on data-entry after completed reading""", ) def __call__(self): """Return a deferred that chains all of the sub-prompts in order Returns from the first of the sub-prompts that recevies a selection returns str(digit) for the key the user pressed """ return self.onNext(None) def onNext(self, result, index=0): """Process the next operation""" if result is not None: return result try: element = self.elements[index] except IndexError as err: # okay, do a waitForDigit from timeout seconds... return self.agi.waitForDigit(self.timeout).addCallback( self.processKey).addCallback(self.processLast) else: df = element.read(self.agi, self.escapeDigits) df.addCallback(self.processKey) df.addCallback(self.onNext, index=index + 1) return df def processKey(self, result): """Does the pressed key belong to escapeDigits?""" if isinstance(result, tuple): # getOption result... if result[1] == 0: # failure during load of the file... log.warn("Apparent failure during load of audio file: %s", self.value) result = 0 else: result = result[0] if isinstance(result, str): if result: result = ord(result) else: result = 0 if result: # None or 0 # User pressed a key during the reading... key = chr(result) if key in self.escapeDigits: log.info('Exiting early due to user press of: %r', key) return key else: # we don't warn user in this menu if they press an unrecognised key! log.info( 'Ignoring user keypress because not in escapeDigits: %r', key) # completed reading without any escape digits, continue reading return None def processLast(self, result): if result is None: result = '' return result