"""
Defines elementary generators

The module gathers together a set of operators that can be combined to 
generate random data values that conform to specifications that are
completely controlled by users

Notes:
 ADDITIONAL GENERATORS
 Link a generator to a distribution
   A number can be integer or float
   A distribution can have a number of different parameters
    rangeGenerator (int, float)

 Maybe it would be useful to be able to return the actual data type 
 that generates the "string" representation so that the data is still 
 computable for intermediate processing steps.
 
 Time series can be stationary or dynamic. Pregnancy rates could be 
 falling (for example) but this generator would generate births at 
 fixed rates.
 
Athanasios Anastasiou Sept 2016
"""

#TODO: Must also create something like Data Serialisers for various formats.

import uuid
import random
import rstr
import datetime
from bunch import Bunch

class randomDataGenerator(object):
    """Base class for data generators
    
    Defines an abstract randomDataGenerator along with its basic data
    and interactions.
    
    A randomDataGenerator is supposed to define a model for the possible
    values generated by the randomDataGenerator.
    
    A randomDataGenerator is NOT supposed to define the values of instances.
    
    Consequently, randomDataGenerators have been modeled as callables. 
    They are instantiated to their internal state and produce an instance
    when they are called.
    
    Please note, this class is not supposed to be initialised directly.
    
    randomDataGenerators can be combined using three operators:
    * (mul),| (logic OR),^ (logic XOR) with the following semantics:
    
    (*) MUL: Cartesian Product
        P = optionGenerator(["Black", "White"])
        Q = optionGenerator(["Dwarf", "Hole"])
        K = P * Q

    (^) XOR: Chained evaluation with equal probabilities
        P = optionGenerator(["Black", "White"])
        Q = optionGenerator(["Hole", "Dwarf"])
        K = P^Q
        
        Upon successive calls, K will be returning "Black, White, Hole, Dwarf" as if it were
        one big optionGenerator(["Black", "White", "Hole", "Dwarf"])
        
    (|) OR: Conditional probability
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.
        
    Standard generators provided by DGen will have well defined semantics
    and combinations via operators (algebra). Derived generators are expected
    to redefine operators if required.
    """
    _name = None
    
    def __init__(self):
        """Standard constructor
        """
        self._name = None
    
    @property
    def name(self):
        """Returns the name of the generator
        """
        return self._name
  
       
    def setVarName(self, theName):
        """Sets the name of the generator
        
        Args:
            theName: A string naming the generator.
            There are no particular specifications for naming variables
            but given that the intention is to use the name to identify
            the variable at serialisation, a name that would not cause 
            problems if it were to be used as an attribute should be selected.
            
        Returns:
            The randomDataGenerator object itself. 
            This is to enable chained application of functions.
        """
        self._name = theName
        return self
        
    def __mul__(self, other):        
        """Instantiates a composite generator as the cartesian product of two others
        
        Args:
            other: A randomDataGenerator being combined with this instance
            
        Returns:
            A compositeORGenerator that produces the cartesian product
            of two simple generators.
        """
        if isinstance(other, basestring):
            #TODO: Any literal should be converted to a constantGenerator for consistency
            other = optionGenerator([other])            
        return compositeORGenerator(left=self, right=other)
        
    def __or__(self,other):
        """Instantiates a conditional generator
        
        Similarly to __mul__ (*), this operator chains together two 
        randomDataGenerators in a specific way. The intention of overiding 
        the or operator (|) is to match the way conditional probabilities
        are expressed in mathematics (please see example further below).
        The consequence of this is that *this* generator, should be a
        condProbOptionGenerator. Otherwise, this combination is undefined.
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            A compositeConditionalGenerator implementing the intended semantics.
        """
        #TODO: Operators should raise an exception if the combination attempted does not make sense.
        return compositeConditionalGenerator(left=self, right=other)
    
    def __xor__(self, other):
        """Instantiates an exclusive OR, randomDataGenerator
        
        This is essentially a convenience operator to package a series of 
        randomDataGenerators to one big optionGenerator.
        Please note, this is different than the __mul__ behaviour
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            An optionGenerator implementing the intended semantics.
        """
        #TODO: Implement l*str, l*randomDataGenerator to mean probability*generator
        return optionGenerator([(0.5,self), (0.5,other)])

class compositeGenerator(randomDataGenerator):    
    """Defines a composite (binary) generator. A generator whose output depends on two unary ones.
    
    A composite generator maintains an internal "list of pointers" to other generators
    and it evaluates them in an appropriate way as dictated by the algebra (i.e. either in 
    conjunction or conditional mode)
    
    Please note, this class is not supposed to be initialised directly.
    """
       
    def __init__(self, left = None, right = None):
        """Instantiates the binary compositeGenerator as the result of two others (the left and right
        in terms of parsing)
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeGenerator
        """
        
        super(compositeGenerator,self).__init__()
        self._left = left
        self._right = right
             
class compositeORGenerator(compositeGenerator):
    """Defines the cartesian product of two randomDataGenerators
    """
    
    def __init__(self, left, right):        
        """Instantiates the binary compositeORGenerator that implements the cartesian product of two randomDataGenerators
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        super(compositeORGenerator,self).__init__()
        self._left = left
        self._right = right
        
    def __mul__(self,other):
        """Extends the cartesian product generator.
        
        This is essentially an extension to the compositeORgenerator that defines
        what happens when compositeORGenerators are combined with each other.
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        return compositeORGenerator(left = self, right = other)
    
    def __call__(self):
        """Produces the cartesian product of the result of two unary generators.
        
        Args:
            None
        
        Returns:
            String
        """
        return self._left() + self._right()
            
class compositeConditionalGenerator(compositeGenerator):
    """Defines a composite generator that implements conditional evaluation
    of randomDataGenerators."""    
    
    def __init__(self, left = None, right = None):
        """Instantiates the binary compositeConditionalGenerator that implements 
        conditional evaluation
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        super(compositeConditionalGenerator,self).__init__()
        self._left = left
        self._right = right
        
    def __or__(self,other):
        """Defines the behaviour of chaining conditionals with the OR operator (|)
        
        This is essentially, the value of **this** generator given the value of the other generator.
        
        Args:
            other: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        #TODO: ensure that the "left" operand is a conditional itself.
        return compositeConditionalGenerator(left = self, right = other)
        
    def __call__(self):
        return self._left(self._right())
    
class constantGenerator(randomDataGenerator):
    """Defines a generator that simply returns a literal
    
    Constant generators are simply wrappers around literals so that they
    can be handled as randomDataGenerators too in expressions.
    """
    def __init__(self, theConstant):
        """Instantiates a constantGenerator
        
        Args:
            theConstant: A literal (e.g. a string)
        
        Returns:
            A constantGenerator
        """
        super(constantGenerator,self).__init__()
        self._theConstant = theConstant
        
    def __call__(self):
        return self._theConstant
        
class optionGenerator(randomDataGenerator):
    """Defines a random generator that produces "events" from a list of possible events
    
    An optionGenerator can be instantiated to an equal or varying probability events.
    
    Examples:
        P = optionGenerator(["Male", "Female"]) #P produces strings "Male", "Female" with equal probabilities
        P = optionGenerator([(0.2, "Male"), (0.8, "Female")]) #P produces strings "Male", "Female" with varying probabilities
    
    WARNING!
        At the moment, the varying probability flavour of optionGenerator does not check if probabilities of all events add up to one!
    
    optionGenerators can also be "chained" to call other optionGenerators creating more complex generators. For example:
    
        P = optionGenerator([optionGenerator("Black", "White"), optionGenerator("42", "3.1415928")])
        #Each event is produced with equal probabilities (e.g. 0.5*0.5=0.25)
        P = optionGenerator([(0.2,optionGenerator("Black", "White")),(0.8,optionGenerator("42","3.1415928"))])
        #Each event is produced with 0.2*0.5 and 0.8*0.5 probabilities.
    """
    
    def __init__(self, theOptions):
        """Instantiates the option generator
        
        Args:
            theOptions: A list of events or a tuple of (prob, event) pairs.
                        An "event" is either a literal (e.g. a string) or an optionGenerator
        
        Returns:
            Nothing
        """
        #TODO: Instead of basestring, check for number literals as well (or rather, anything that is NOT a randomDataGenerator needs to be wraped in one
        #TODO: Sum the probabilities to make sure they add up to 1 or throw an exception if they don't
        super(optionGenerator,self).__init__()
        self._Noptions = len(theOptions)        
        if not self._Noptions:
            self._options = []
            return
        #If this is not a list of tuples then it is assumed that each event is assigned an equal probability        
        if not isinstance(theOptions[0],tuple):
            theOptions = [(1.0 / self._Noptions, x) for x in theOptions]
        #Now make sure that if you have single strings appearing they are converted to generators internally
        theOptions = [(x[0], x[1] if not isinstance(x[1],basestring) else constantGenerator(x[1])) for x in theOptions]            
        self._options = theOptions        
    
    def __call__(self):
        """Evaluates the optionGenerator.
        
        Picks an event from the list of events proportional to its probability of appearance.
        """
        v = random.random()
        k=0
        while v>=self._options[k][0]:
            v-=self._options[k][0]
            k+=1
        return self._options[k][1]()
          
class condProbOptionGenerator(compositeConditionalGenerator):
    """Defines a conditional probability generator
    
    A conditional probability generator enables the creation of complex "tree-like"
    evaluations of other randomDataGenerators.
    
    Example:
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.

    Please note:
        This is the only elementary data generator, whose evaluation
        requires a parameter. Using the (|) operator, this is hidden away
        but if required to be evaluated directly, then simply "calling"
        the generator will not work.
    """
    def __init__(self,theOptions):
        """Instantiates the condProbGenerator
        
        Args:
            theOptions: A dict of key:event, value:randomDataGenerator
            
        Returns:
            Nothing
        """
        super(condProbOptionGenerator,self).__init__()
        self._options = theOptions
        self._Noptions = len(self._options)
        
    def __call__(self, givenEvent):
        """Evaluates this randomDataGenerator GIVEN the value of givenEvent.
        
        Args:
            givenEvent: The event that drives the generation of instances from this generator.
            
        Returns:
            The evaluation of a generator, given givenEvent
        
        Please note:
            This is the only elementary data generator, whose evaluation
            requires a parameter.
        """
        return self._options[givenEvent]()
            

class archivedOptionGenerator(optionGenerator):
    """Defines an option generator that is instantiated by a file
    
    This is a very simple convenience class that essentially instantiates an optionGenerator with 
    data read from a file. Consequently, only strings (and probabilities of strings) can be described.
    
    Examples:
        Given a "data.csv" file:
        P = archivedOptionGenerator("data.csv")
        
        The format of the CSV file is either a single column of strings (each string in its own row)
        or pairs of prob,string (each pair in its own row).
        
        Lines begining with # are ignored (Taken as comments)
    """
    def __init__(self,aFilename):        
        """Instantiates the generator with a file from the disk"""
        super(archivedOptionGenerator,self).__init__([])
        self._fromFile = aFilename
        fd = open(aFilename,"r")
        self._options = filter(lambda x:not x.startswith("#"),map(lambda x:x[0:-1],fd.readlines()))
        fd.close()
        self._Noptions = len(self._options)       
        #If there is a coma on the first line, it is assumed that all following lines contain a coma and that the first number is the probability of the event hapenning
        if self._options[0].find(",")>=0:
            newOptions = []
            for anOption in self._options:
                splitOption = anOption.split(",")
                newOptions.append((float(splitOption[0]),splitOption[1].replace("\n","") if not isinstance(splitOption[1],basestring) else constantGenerator(splitOption[1].replace("\n",""))))
            self._options = newOptions
        else:
            self._options = map(lambda x:(1.0 / self._Noptions, constantGenerator(x.replace("\n",""))),self._options)
            
class revRegexGenerator(randomDataGenerator):
    """Defines a reverse regular expression generator
    
    A reverse regular expression generator simply takes the definition of 
    the string and produces all strings that match it.
    
    This class relies on the excellent rstr module (https://pypi.python.org/pypi/rstr/2.1.3) for its output.
    
    Examples:
        P = revRegexGenerator("[0-9A-F][0-9A-f][0-9A-F][0-9A-F]")
        
        Upon successive calls to P, a new 4 digit hexadecial number will be
        produced.
        
    WARNING!!!
        If you P = revRegexGenerator("[0-9]*"), you are on your own.
    """
    def __init__(self,revRegex):
        """Instantiates the generator with a regex that is used to generate the string
        
        Args:
            revRegex: A string with the regular expression definition
        
        Returns:
            Nothing
        """
        super(revRegexGenerator,self).__init__()
        self._xeger = revRegex
        
    def __call__(self):
        """Evaluates the output of the generator"""
        return rstr.xeger(self._xeger)
        
class uidGenerator(randomDataGenerator):
    """Defines a randomDataGenerator that returns Universal Unique IDentifiers (UUID)
    
    This class relies on the standard uuid module (https://pypi.python.org/pypi/rstr/2.1.3) 
    which is included in the standard distribution of Python since version 2.5
    
    Examples:
        P = uidGenerator()
        
    """
    def __init__(self):
        """Instantiates the UUID generator"""
        super(uidGenerator,self).__init__()
        
    def __call__(self):
        return str(uuid.uuid4())
        
class seqGenerator(randomDataGenerator):
    """Defines a randomDataGenerator to generate sequences of characters
    
    This is essentially a "helper" class to generate random sequences of characters
    that again relies heavily on rstr (please also see documentation of revRegexGenerator).
    
    Examples:
        P = seqGenerator("ABCDEFGHIJKLMNOPQRST", maxNum=12)
        
        This will generate sequences of A-T characters which are 12 characters long.
    """
    def __init__(self, setOfChars, maxNum=1):
        """Instantiates the seqGenerator
        
        Args:
            setOfChars: A string with all the possible characters in the sequence
            maxNum: An integer describing the number of chars in the sequence
            
        Returns:
            Nothing
        """
        super(seqGenerator,self).__init__()
        self._theSetOfChars = setOfChars
        self._maxNum = maxNum
        
    def __call__(self):
        return rstr.rstr(self._theSetOfChars,self._maxNum)
        
        
class dateGenerator(randomDataGenerator):
    """Defines a generator that returns a random date between two dates
    
    The class relies on the datetime module (https://docs.python.org/2/library/datetime.html)
    to generate a random valid date.
    
    Examples:
        P = dateGenerator(datetime.datetime.now()-datetime.timeinterval(weeks-4), datetime.datetime.now())
        
        This will generate a date within the last month.
        
    WARNING!!!
        At the moment no validation is performed on the datetime objects passed to this function
    """
    def __init__(self, dateStart, dateEnd):
        """Instantiates the random date generator with a start and end date
        
        Args:
            dateStart, dateEnd: datetime objects describing the date interval
        Returns:
            Nothing
        """        
        #TODO: Raise an exception if dateEnd<dateStart.
        super(dateGenerator,self).__init__()
        d = dateEnd - dateStart
        self._startDate = dateStart
        self._dateDiffSeconds = d.days * 86400 + d.seconds        
                
    def __call__(self):
        return str(self._startDate + datetime.timedelta(seconds = random.randrange(self._dateDiffSeconds)))

37c29408eefc9e7053c1cca31de58d8b1361ff87
<mask token>


class constantGenerator(randomDataGenerator):
    """Defines a generator that simply returns a literal
    
    Constant generators are simply wrappers around literals so that they
    can be handled as randomDataGenerators too in expressions.
    """

    def __init__(self, theConstant):
        """Instantiates a constantGenerator
        
        Args:
            theConstant: A literal (e.g. a string)
        
        Returns:
            A constantGenerator
        """
        super(constantGenerator, self).__init__()
        self._theConstant = theConstant

    def __call__(self):
        return self._theConstant


class optionGenerator(randomDataGenerator):
    """Defines a random generator that produces "events" from a list of possible events
    
    An optionGenerator can be instantiated to an equal or varying probability events.
    
    Examples:
        P = optionGenerator(["Male", "Female"]) #P produces strings "Male", "Female" with equal probabilities
        P = optionGenerator([(0.2, "Male"), (0.8, "Female")]) #P produces strings "Male", "Female" with varying probabilities
    
    WARNING!
        At the moment, the varying probability flavour of optionGenerator does not check if probabilities of all events add up to one!
    
    optionGenerators can also be "chained" to call other optionGenerators creating more complex generators. For example:
    
        P = optionGenerator([optionGenerator("Black", "White"), optionGenerator("42", "3.1415928")])
        #Each event is produced with equal probabilities (e.g. 0.5*0.5=0.25)
        P = optionGenerator([(0.2,optionGenerator("Black", "White")),(0.8,optionGenerator("42","3.1415928"))])
        #Each event is produced with 0.2*0.5 and 0.8*0.5 probabilities.
    """

    def __init__(self, theOptions):
        """Instantiates the option generator
        
        Args:
            theOptions: A list of events or a tuple of (prob, event) pairs.
                        An "event" is either a literal (e.g. a string) or an optionGenerator
        
        Returns:
            Nothing
        """
        super(optionGenerator, self).__init__()
        self._Noptions = len(theOptions)
        if not self._Noptions:
            self._options = []
            return
        if not isinstance(theOptions[0], tuple):
            theOptions = [(1.0 / self._Noptions, x) for x in theOptions]
        theOptions = [(x[0], x[1] if not isinstance(x[1], basestring) else
            constantGenerator(x[1])) for x in theOptions]
        self._options = theOptions

    def __call__(self):
        """Evaluates the optionGenerator.
        
        Picks an event from the list of events proportional to its probability of appearance.
        """
        v = random.random()
        k = 0
        while v >= self._options[k][0]:
            v -= self._options[k][0]
            k += 1
        return self._options[k][1]()


class condProbOptionGenerator(compositeConditionalGenerator):
    """Defines a conditional probability generator
    
    A conditional probability generator enables the creation of complex "tree-like"
    evaluations of other randomDataGenerators.
    
    Example:
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.

    Please note:
        This is the only elementary data generator, whose evaluation
        requires a parameter. Using the (|) operator, this is hidden away
        but if required to be evaluated directly, then simply "calling"
        the generator will not work.
    """

    def __init__(self, theOptions):
        """Instantiates the condProbGenerator
        
        Args:
            theOptions: A dict of key:event, value:randomDataGenerator
            
        Returns:
            Nothing
        """
        super(condProbOptionGenerator, self).__init__()
        self._options = theOptions
        self._Noptions = len(self._options)

    def __call__(self, givenEvent):
        """Evaluates this randomDataGenerator GIVEN the value of givenEvent.
        
        Args:
            givenEvent: The event that drives the generation of instances from this generator.
            
        Returns:
            The evaluation of a generator, given givenEvent
        
        Please note:
            This is the only elementary data generator, whose evaluation
            requires a parameter.
        """
        return self._options[givenEvent]()


class archivedOptionGenerator(optionGenerator):
    """Defines an option generator that is instantiated by a file
    
    This is a very simple convenience class that essentially instantiates an optionGenerator with 
    data read from a file. Consequently, only strings (and probabilities of strings) can be described.
    
    Examples:
        Given a "data.csv" file:
        P = archivedOptionGenerator("data.csv")
        
        The format of the CSV file is either a single column of strings (each string in its own row)
        or pairs of prob,string (each pair in its own row).
        
        Lines begining with # are ignored (Taken as comments)
    """

    def __init__(self, aFilename):
        """Instantiates the generator with a file from the disk"""
        super(archivedOptionGenerator, self).__init__([])
        self._fromFile = aFilename
        fd = open(aFilename, 'r')
        self._options = filter(lambda x: not x.startswith('#'), map(lambda
            x: x[0:-1], fd.readlines()))
        fd.close()
        self._Noptions = len(self._options)
        if self._options[0].find(',') >= 0:
            newOptions = []
            for anOption in self._options:
                splitOption = anOption.split(',')
                newOptions.append((float(splitOption[0]), splitOption[1].
                    replace('\n', '') if not isinstance(splitOption[1],
                    basestring) else constantGenerator(splitOption[1].
                    replace('\n', ''))))
            self._options = newOptions
        else:
            self._options = map(lambda x: (1.0 / self._Noptions,
                constantGenerator(x.replace('\n', ''))), self._options)


class revRegexGenerator(randomDataGenerator):
    """Defines a reverse regular expression generator
    
    A reverse regular expression generator simply takes the definition of 
    the string and produces all strings that match it.
    
    This class relies on the excellent rstr module (https://pypi.python.org/pypi/rstr/2.1.3) for its output.
    
    Examples:
        P = revRegexGenerator("[0-9A-F][0-9A-f][0-9A-F][0-9A-F]")
        
        Upon successive calls to P, a new 4 digit hexadecial number will be
        produced.
        
    WARNING!!!
        If you P = revRegexGenerator("[0-9]*"), you are on your own.
    """

    def __init__(self, revRegex):
        """Instantiates the generator with a regex that is used to generate the string
        
        Args:
            revRegex: A string with the regular expression definition
        
        Returns:
            Nothing
        """
        super(revRegexGenerator, self).__init__()
        self._xeger = revRegex

    def __call__(self):
        """Evaluates the output of the generator"""
        return rstr.xeger(self._xeger)


class uidGenerator(randomDataGenerator):
    """Defines a randomDataGenerator that returns Universal Unique IDentifiers (UUID)
    
    This class relies on the standard uuid module (https://pypi.python.org/pypi/rstr/2.1.3) 
    which is included in the standard distribution of Python since version 2.5
    
    Examples:
        P = uidGenerator()
        
    """

    def __init__(self):
        """Instantiates the UUID generator"""
        super(uidGenerator, self).__init__()

    def __call__(self):
        return str(uuid.uuid4())


class seqGenerator(randomDataGenerator):
    """Defines a randomDataGenerator to generate sequences of characters
    
    This is essentially a "helper" class to generate random sequences of characters
    that again relies heavily on rstr (please also see documentation of revRegexGenerator).
    
    Examples:
        P = seqGenerator("ABCDEFGHIJKLMNOPQRST", maxNum=12)
        
        This will generate sequences of A-T characters which are 12 characters long.
    """

    def __init__(self, setOfChars, maxNum=1):
        """Instantiates the seqGenerator
        
        Args:
            setOfChars: A string with all the possible characters in the sequence
            maxNum: An integer describing the number of chars in the sequence
            
        Returns:
            Nothing
        """
        super(seqGenerator, self).__init__()
        self._theSetOfChars = setOfChars
        self._maxNum = maxNum

    def __call__(self):
        return rstr.rstr(self._theSetOfChars, self._maxNum)


class dateGenerator(randomDataGenerator):
    """Defines a generator that returns a random date between two dates
    
    The class relies on the datetime module (https://docs.python.org/2/library/datetime.html)
    to generate a random valid date.
    
    Examples:
        P = dateGenerator(datetime.datetime.now()-datetime.timeinterval(weeks-4), datetime.datetime.now())
        
        This will generate a date within the last month.
        
    WARNING!!!
        At the moment no validation is performed on the datetime objects passed to this function
    """

    def __init__(self, dateStart, dateEnd):
        """Instantiates the random date generator with a start and end date
        
        Args:
            dateStart, dateEnd: datetime objects describing the date interval
        Returns:
            Nothing
        """
        super(dateGenerator, self).__init__()
        d = dateEnd - dateStart
        self._startDate = dateStart
        self._dateDiffSeconds = d.days * 86400 + d.seconds

    def __call__(self):
        return str(self._startDate + datetime.timedelta(seconds=random.
            randrange(self._dateDiffSeconds)))

<mask token>


class randomDataGenerator(object):
    <mask token>
    <mask token>

    def __init__(self):
        """Standard constructor
        """
        self._name = None

    @property
    def name(self):
        """Returns the name of the generator
        """
        return self._name

    def setVarName(self, theName):
        """Sets the name of the generator
        
        Args:
            theName: A string naming the generator.
            There are no particular specifications for naming variables
            but given that the intention is to use the name to identify
            the variable at serialisation, a name that would not cause 
            problems if it were to be used as an attribute should be selected.
            
        Returns:
            The randomDataGenerator object itself. 
            This is to enable chained application of functions.
        """
        self._name = theName
        return self

    def __mul__(self, other):
        """Instantiates a composite generator as the cartesian product of two others
        
        Args:
            other: A randomDataGenerator being combined with this instance
            
        Returns:
            A compositeORGenerator that produces the cartesian product
            of two simple generators.
        """
        if isinstance(other, basestring):
            other = optionGenerator([other])
        return compositeORGenerator(left=self, right=other)

    def __or__(self, other):
        """Instantiates a conditional generator
        
        Similarly to __mul__ (*), this operator chains together two 
        randomDataGenerators in a specific way. The intention of overiding 
        the or operator (|) is to match the way conditional probabilities
        are expressed in mathematics (please see example further below).
        The consequence of this is that *this* generator, should be a
        condProbOptionGenerator. Otherwise, this combination is undefined.
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            A compositeConditionalGenerator implementing the intended semantics.
        """
        return compositeConditionalGenerator(left=self, right=other)

    def __xor__(self, other):
        """Instantiates an exclusive OR, randomDataGenerator
        
        This is essentially a convenience operator to package a series of 
        randomDataGenerators to one big optionGenerator.
        Please note, this is different than the __mul__ behaviour
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            An optionGenerator implementing the intended semantics.
        """
        return optionGenerator([(0.5, self), (0.5, other)])


class compositeGenerator(randomDataGenerator):
    """Defines a composite (binary) generator. A generator whose output depends on two unary ones.
    
    A composite generator maintains an internal "list of pointers" to other generators
    and it evaluates them in an appropriate way as dictated by the algebra (i.e. either in 
    conjunction or conditional mode)
    
    Please note, this class is not supposed to be initialised directly.
    """

    def __init__(self, left=None, right=None):
        """Instantiates the binary compositeGenerator as the result of two others (the left and right
        in terms of parsing)
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeGenerator
        """
        super(compositeGenerator, self).__init__()
        self._left = left
        self._right = right


class compositeORGenerator(compositeGenerator):
    """Defines the cartesian product of two randomDataGenerators
    """

    def __init__(self, left, right):
        """Instantiates the binary compositeORGenerator that implements the cartesian product of two randomDataGenerators
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        super(compositeORGenerator, self).__init__()
        self._left = left
        self._right = right

    def __mul__(self, other):
        """Extends the cartesian product generator.
        
        This is essentially an extension to the compositeORgenerator that defines
        what happens when compositeORGenerators are combined with each other.
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        return compositeORGenerator(left=self, right=other)

    def __call__(self):
        """Produces the cartesian product of the result of two unary generators.
        
        Args:
            None
        
        Returns:
            String
        """
        return self._left() + self._right()


class compositeConditionalGenerator(compositeGenerator):
    """Defines a composite generator that implements conditional evaluation
    of randomDataGenerators."""

    def __init__(self, left=None, right=None):
        """Instantiates the binary compositeConditionalGenerator that implements 
        conditional evaluation
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        super(compositeConditionalGenerator, self).__init__()
        self._left = left
        self._right = right

    def __or__(self, other):
        """Defines the behaviour of chaining conditionals with the OR operator (|)
        
        This is essentially, the value of **this** generator given the value of the other generator.
        
        Args:
            other: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        return compositeConditionalGenerator(left=self, right=other)

    def __call__(self):
        return self._left(self._right())


class constantGenerator(randomDataGenerator):
    """Defines a generator that simply returns a literal
    
    Constant generators are simply wrappers around literals so that they
    can be handled as randomDataGenerators too in expressions.
    """

    def __init__(self, theConstant):
        """Instantiates a constantGenerator
        
        Args:
            theConstant: A literal (e.g. a string)
        
        Returns:
            A constantGenerator
        """
        super(constantGenerator, self).__init__()
        self._theConstant = theConstant

    def __call__(self):
        return self._theConstant


class optionGenerator(randomDataGenerator):
    """Defines a random generator that produces "events" from a list of possible events
    
    An optionGenerator can be instantiated to an equal or varying probability events.
    
    Examples:
        P = optionGenerator(["Male", "Female"]) #P produces strings "Male", "Female" with equal probabilities
        P = optionGenerator([(0.2, "Male"), (0.8, "Female")]) #P produces strings "Male", "Female" with varying probabilities
    
    WARNING!
        At the moment, the varying probability flavour of optionGenerator does not check if probabilities of all events add up to one!
    
    optionGenerators can also be "chained" to call other optionGenerators creating more complex generators. For example:
    
        P = optionGenerator([optionGenerator("Black", "White"), optionGenerator("42", "3.1415928")])
        #Each event is produced with equal probabilities (e.g. 0.5*0.5=0.25)
        P = optionGenerator([(0.2,optionGenerator("Black", "White")),(0.8,optionGenerator("42","3.1415928"))])
        #Each event is produced with 0.2*0.5 and 0.8*0.5 probabilities.
    """

    def __init__(self, theOptions):
        """Instantiates the option generator
        
        Args:
            theOptions: A list of events or a tuple of (prob, event) pairs.
                        An "event" is either a literal (e.g. a string) or an optionGenerator
        
        Returns:
            Nothing
        """
        super(optionGenerator, self).__init__()
        self._Noptions = len(theOptions)
        if not self._Noptions:
            self._options = []
            return
        if not isinstance(theOptions[0], tuple):
            theOptions = [(1.0 / self._Noptions, x) for x in theOptions]
        theOptions = [(x[0], x[1] if not isinstance(x[1], basestring) else
            constantGenerator(x[1])) for x in theOptions]
        self._options = theOptions

    def __call__(self):
        """Evaluates the optionGenerator.
        
        Picks an event from the list of events proportional to its probability of appearance.
        """
        v = random.random()
        k = 0
        while v >= self._options[k][0]:
            v -= self._options[k][0]
            k += 1
        return self._options[k][1]()


class condProbOptionGenerator(compositeConditionalGenerator):
    """Defines a conditional probability generator
    
    A conditional probability generator enables the creation of complex "tree-like"
    evaluations of other randomDataGenerators.
    
    Example:
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.

    Please note:
        This is the only elementary data generator, whose evaluation
        requires a parameter. Using the (|) operator, this is hidden away
        but if required to be evaluated directly, then simply "calling"
        the generator will not work.
    """

    def __init__(self, theOptions):
        """Instantiates the condProbGenerator
        
        Args:
            theOptions: A dict of key:event, value:randomDataGenerator
            
        Returns:
            Nothing
        """
        super(condProbOptionGenerator, self).__init__()
        self._options = theOptions
        self._Noptions = len(self._options)

    def __call__(self, givenEvent):
        """Evaluates this randomDataGenerator GIVEN the value of givenEvent.
        
        Args:
            givenEvent: The event that drives the generation of instances from this generator.
            
        Returns:
            The evaluation of a generator, given givenEvent
        
        Please note:
            This is the only elementary data generator, whose evaluation
            requires a parameter.
        """
        return self._options[givenEvent]()


class archivedOptionGenerator(optionGenerator):
    """Defines an option generator that is instantiated by a file
    
    This is a very simple convenience class that essentially instantiates an optionGenerator with 
    data read from a file. Consequently, only strings (and probabilities of strings) can be described.
    
    Examples:
        Given a "data.csv" file:
        P = archivedOptionGenerator("data.csv")
        
        The format of the CSV file is either a single column of strings (each string in its own row)
        or pairs of prob,string (each pair in its own row).
        
        Lines begining with # are ignored (Taken as comments)
    """

    def __init__(self, aFilename):
        """Instantiates the generator with a file from the disk"""
        super(archivedOptionGenerator, self).__init__([])
        self._fromFile = aFilename
        fd = open(aFilename, 'r')
        self._options = filter(lambda x: not x.startswith('#'), map(lambda
            x: x[0:-1], fd.readlines()))
        fd.close()
        self._Noptions = len(self._options)
        if self._options[0].find(',') >= 0:
            newOptions = []
            for anOption in self._options:
                splitOption = anOption.split(',')
                newOptions.append((float(splitOption[0]), splitOption[1].
                    replace('\n', '') if not isinstance(splitOption[1],
                    basestring) else constantGenerator(splitOption[1].
                    replace('\n', ''))))
            self._options = newOptions
        else:
            self._options = map(lambda x: (1.0 / self._Noptions,
                constantGenerator(x.replace('\n', ''))), self._options)


class revRegexGenerator(randomDataGenerator):
    """Defines a reverse regular expression generator
    
    A reverse regular expression generator simply takes the definition of 
    the string and produces all strings that match it.
    
    This class relies on the excellent rstr module (https://pypi.python.org/pypi/rstr/2.1.3) for its output.
    
    Examples:
        P = revRegexGenerator("[0-9A-F][0-9A-f][0-9A-F][0-9A-F]")
        
        Upon successive calls to P, a new 4 digit hexadecial number will be
        produced.
        
    WARNING!!!
        If you P = revRegexGenerator("[0-9]*"), you are on your own.
    """

    def __init__(self, revRegex):
        """Instantiates the generator with a regex that is used to generate the string
        
        Args:
            revRegex: A string with the regular expression definition
        
        Returns:
            Nothing
        """
        super(revRegexGenerator, self).__init__()
        self._xeger = revRegex

    def __call__(self):
        """Evaluates the output of the generator"""
        return rstr.xeger(self._xeger)


class uidGenerator(randomDataGenerator):
    """Defines a randomDataGenerator that returns Universal Unique IDentifiers (UUID)
    
    This class relies on the standard uuid module (https://pypi.python.org/pypi/rstr/2.1.3) 
    which is included in the standard distribution of Python since version 2.5
    
    Examples:
        P = uidGenerator()
        
    """

    def __init__(self):
        """Instantiates the UUID generator"""
        super(uidGenerator, self).__init__()

    def __call__(self):
        return str(uuid.uuid4())


class seqGenerator(randomDataGenerator):
    """Defines a randomDataGenerator to generate sequences of characters
    
    This is essentially a "helper" class to generate random sequences of characters
    that again relies heavily on rstr (please also see documentation of revRegexGenerator).
    
    Examples:
        P = seqGenerator("ABCDEFGHIJKLMNOPQRST", maxNum=12)
        
        This will generate sequences of A-T characters which are 12 characters long.
    """

    def __init__(self, setOfChars, maxNum=1):
        """Instantiates the seqGenerator
        
        Args:
            setOfChars: A string with all the possible characters in the sequence
            maxNum: An integer describing the number of chars in the sequence
            
        Returns:
            Nothing
        """
        super(seqGenerator, self).__init__()
        self._theSetOfChars = setOfChars
        self._maxNum = maxNum

    def __call__(self):
        return rstr.rstr(self._theSetOfChars, self._maxNum)


class dateGenerator(randomDataGenerator):
    """Defines a generator that returns a random date between two dates
    
    The class relies on the datetime module (https://docs.python.org/2/library/datetime.html)
    to generate a random valid date.
    
    Examples:
        P = dateGenerator(datetime.datetime.now()-datetime.timeinterval(weeks-4), datetime.datetime.now())
        
        This will generate a date within the last month.
        
    WARNING!!!
        At the moment no validation is performed on the datetime objects passed to this function
    """

    def __init__(self, dateStart, dateEnd):
        """Instantiates the random date generator with a start and end date
        
        Args:
            dateStart, dateEnd: datetime objects describing the date interval
        Returns:
            Nothing
        """
        super(dateGenerator, self).__init__()
        d = dateEnd - dateStart
        self._startDate = dateStart
        self._dateDiffSeconds = d.days * 86400 + d.seconds

    def __call__(self):
        return str(self._startDate + datetime.timedelta(seconds=random.
            randrange(self._dateDiffSeconds)))

<mask token>


class randomDataGenerator(object):
    <mask token>
    _name = None

    def __init__(self):
        """Standard constructor
        """
        self._name = None

    @property
    def name(self):
        """Returns the name of the generator
        """
        return self._name

    def setVarName(self, theName):
        """Sets the name of the generator
        
        Args:
            theName: A string naming the generator.
            There are no particular specifications for naming variables
            but given that the intention is to use the name to identify
            the variable at serialisation, a name that would not cause 
            problems if it were to be used as an attribute should be selected.
            
        Returns:
            The randomDataGenerator object itself. 
            This is to enable chained application of functions.
        """
        self._name = theName
        return self

    def __mul__(self, other):
        """Instantiates a composite generator as the cartesian product of two others
        
        Args:
            other: A randomDataGenerator being combined with this instance
            
        Returns:
            A compositeORGenerator that produces the cartesian product
            of two simple generators.
        """
        if isinstance(other, basestring):
            other = optionGenerator([other])
        return compositeORGenerator(left=self, right=other)

    def __or__(self, other):
        """Instantiates a conditional generator
        
        Similarly to __mul__ (*), this operator chains together two 
        randomDataGenerators in a specific way. The intention of overiding 
        the or operator (|) is to match the way conditional probabilities
        are expressed in mathematics (please see example further below).
        The consequence of this is that *this* generator, should be a
        condProbOptionGenerator. Otherwise, this combination is undefined.
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            A compositeConditionalGenerator implementing the intended semantics.
        """
        return compositeConditionalGenerator(left=self, right=other)

    def __xor__(self, other):
        """Instantiates an exclusive OR, randomDataGenerator
        
        This is essentially a convenience operator to package a series of 
        randomDataGenerators to one big optionGenerator.
        Please note, this is different than the __mul__ behaviour
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            An optionGenerator implementing the intended semantics.
        """
        return optionGenerator([(0.5, self), (0.5, other)])


class compositeGenerator(randomDataGenerator):
    """Defines a composite (binary) generator. A generator whose output depends on two unary ones.
    
    A composite generator maintains an internal "list of pointers" to other generators
    and it evaluates them in an appropriate way as dictated by the algebra (i.e. either in 
    conjunction or conditional mode)
    
    Please note, this class is not supposed to be initialised directly.
    """

    def __init__(self, left=None, right=None):
        """Instantiates the binary compositeGenerator as the result of two others (the left and right
        in terms of parsing)
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeGenerator
        """
        super(compositeGenerator, self).__init__()
        self._left = left
        self._right = right


class compositeORGenerator(compositeGenerator):
    """Defines the cartesian product of two randomDataGenerators
    """

    def __init__(self, left, right):
        """Instantiates the binary compositeORGenerator that implements the cartesian product of two randomDataGenerators
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        super(compositeORGenerator, self).__init__()
        self._left = left
        self._right = right

    def __mul__(self, other):
        """Extends the cartesian product generator.
        
        This is essentially an extension to the compositeORgenerator that defines
        what happens when compositeORGenerators are combined with each other.
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        return compositeORGenerator(left=self, right=other)

    def __call__(self):
        """Produces the cartesian product of the result of two unary generators.
        
        Args:
            None
        
        Returns:
            String
        """
        return self._left() + self._right()


class compositeConditionalGenerator(compositeGenerator):
    """Defines a composite generator that implements conditional evaluation
    of randomDataGenerators."""

    def __init__(self, left=None, right=None):
        """Instantiates the binary compositeConditionalGenerator that implements 
        conditional evaluation
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        super(compositeConditionalGenerator, self).__init__()
        self._left = left
        self._right = right

    def __or__(self, other):
        """Defines the behaviour of chaining conditionals with the OR operator (|)
        
        This is essentially, the value of **this** generator given the value of the other generator.
        
        Args:
            other: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        return compositeConditionalGenerator(left=self, right=other)

    def __call__(self):
        return self._left(self._right())


class constantGenerator(randomDataGenerator):
    """Defines a generator that simply returns a literal
    
    Constant generators are simply wrappers around literals so that they
    can be handled as randomDataGenerators too in expressions.
    """

    def __init__(self, theConstant):
        """Instantiates a constantGenerator
        
        Args:
            theConstant: A literal (e.g. a string)
        
        Returns:
            A constantGenerator
        """
        super(constantGenerator, self).__init__()
        self._theConstant = theConstant

    def __call__(self):
        return self._theConstant


class optionGenerator(randomDataGenerator):
    """Defines a random generator that produces "events" from a list of possible events
    
    An optionGenerator can be instantiated to an equal or varying probability events.
    
    Examples:
        P = optionGenerator(["Male", "Female"]) #P produces strings "Male", "Female" with equal probabilities
        P = optionGenerator([(0.2, "Male"), (0.8, "Female")]) #P produces strings "Male", "Female" with varying probabilities
    
    WARNING!
        At the moment, the varying probability flavour of optionGenerator does not check if probabilities of all events add up to one!
    
    optionGenerators can also be "chained" to call other optionGenerators creating more complex generators. For example:
    
        P = optionGenerator([optionGenerator("Black", "White"), optionGenerator("42", "3.1415928")])
        #Each event is produced with equal probabilities (e.g. 0.5*0.5=0.25)
        P = optionGenerator([(0.2,optionGenerator("Black", "White")),(0.8,optionGenerator("42","3.1415928"))])
        #Each event is produced with 0.2*0.5 and 0.8*0.5 probabilities.
    """

    def __init__(self, theOptions):
        """Instantiates the option generator
        
        Args:
            theOptions: A list of events or a tuple of (prob, event) pairs.
                        An "event" is either a literal (e.g. a string) or an optionGenerator
        
        Returns:
            Nothing
        """
        super(optionGenerator, self).__init__()
        self._Noptions = len(theOptions)
        if not self._Noptions:
            self._options = []
            return
        if not isinstance(theOptions[0], tuple):
            theOptions = [(1.0 / self._Noptions, x) for x in theOptions]
        theOptions = [(x[0], x[1] if not isinstance(x[1], basestring) else
            constantGenerator(x[1])) for x in theOptions]
        self._options = theOptions

    def __call__(self):
        """Evaluates the optionGenerator.
        
        Picks an event from the list of events proportional to its probability of appearance.
        """
        v = random.random()
        k = 0
        while v >= self._options[k][0]:
            v -= self._options[k][0]
            k += 1
        return self._options[k][1]()


class condProbOptionGenerator(compositeConditionalGenerator):
    """Defines a conditional probability generator
    
    A conditional probability generator enables the creation of complex "tree-like"
    evaluations of other randomDataGenerators.
    
    Example:
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.

    Please note:
        This is the only elementary data generator, whose evaluation
        requires a parameter. Using the (|) operator, this is hidden away
        but if required to be evaluated directly, then simply "calling"
        the generator will not work.
    """

    def __init__(self, theOptions):
        """Instantiates the condProbGenerator
        
        Args:
            theOptions: A dict of key:event, value:randomDataGenerator
            
        Returns:
            Nothing
        """
        super(condProbOptionGenerator, self).__init__()
        self._options = theOptions
        self._Noptions = len(self._options)

    def __call__(self, givenEvent):
        """Evaluates this randomDataGenerator GIVEN the value of givenEvent.
        
        Args:
            givenEvent: The event that drives the generation of instances from this generator.
            
        Returns:
            The evaluation of a generator, given givenEvent
        
        Please note:
            This is the only elementary data generator, whose evaluation
            requires a parameter.
        """
        return self._options[givenEvent]()


class archivedOptionGenerator(optionGenerator):
    """Defines an option generator that is instantiated by a file
    
    This is a very simple convenience class that essentially instantiates an optionGenerator with 
    data read from a file. Consequently, only strings (and probabilities of strings) can be described.
    
    Examples:
        Given a "data.csv" file:
        P = archivedOptionGenerator("data.csv")
        
        The format of the CSV file is either a single column of strings (each string in its own row)
        or pairs of prob,string (each pair in its own row).
        
        Lines begining with # are ignored (Taken as comments)
    """

    def __init__(self, aFilename):
        """Instantiates the generator with a file from the disk"""
        super(archivedOptionGenerator, self).__init__([])
        self._fromFile = aFilename
        fd = open(aFilename, 'r')
        self._options = filter(lambda x: not x.startswith('#'), map(lambda
            x: x[0:-1], fd.readlines()))
        fd.close()
        self._Noptions = len(self._options)
        if self._options[0].find(',') >= 0:
            newOptions = []
            for anOption in self._options:
                splitOption = anOption.split(',')
                newOptions.append((float(splitOption[0]), splitOption[1].
                    replace('\n', '') if not isinstance(splitOption[1],
                    basestring) else constantGenerator(splitOption[1].
                    replace('\n', ''))))
            self._options = newOptions
        else:
            self._options = map(lambda x: (1.0 / self._Noptions,
                constantGenerator(x.replace('\n', ''))), self._options)


class revRegexGenerator(randomDataGenerator):
    """Defines a reverse regular expression generator
    
    A reverse regular expression generator simply takes the definition of 
    the string and produces all strings that match it.
    
    This class relies on the excellent rstr module (https://pypi.python.org/pypi/rstr/2.1.3) for its output.
    
    Examples:
        P = revRegexGenerator("[0-9A-F][0-9A-f][0-9A-F][0-9A-F]")
        
        Upon successive calls to P, a new 4 digit hexadecial number will be
        produced.
        
    WARNING!!!
        If you P = revRegexGenerator("[0-9]*"), you are on your own.
    """

    def __init__(self, revRegex):
        """Instantiates the generator with a regex that is used to generate the string
        
        Args:
            revRegex: A string with the regular expression definition
        
        Returns:
            Nothing
        """
        super(revRegexGenerator, self).__init__()
        self._xeger = revRegex

    def __call__(self):
        """Evaluates the output of the generator"""
        return rstr.xeger(self._xeger)


class uidGenerator(randomDataGenerator):
    """Defines a randomDataGenerator that returns Universal Unique IDentifiers (UUID)
    
    This class relies on the standard uuid module (https://pypi.python.org/pypi/rstr/2.1.3) 
    which is included in the standard distribution of Python since version 2.5
    
    Examples:
        P = uidGenerator()
        
    """

    def __init__(self):
        """Instantiates the UUID generator"""
        super(uidGenerator, self).__init__()

    def __call__(self):
        return str(uuid.uuid4())


class seqGenerator(randomDataGenerator):
    """Defines a randomDataGenerator to generate sequences of characters
    
    This is essentially a "helper" class to generate random sequences of characters
    that again relies heavily on rstr (please also see documentation of revRegexGenerator).
    
    Examples:
        P = seqGenerator("ABCDEFGHIJKLMNOPQRST", maxNum=12)
        
        This will generate sequences of A-T characters which are 12 characters long.
    """

    def __init__(self, setOfChars, maxNum=1):
        """Instantiates the seqGenerator
        
        Args:
            setOfChars: A string with all the possible characters in the sequence
            maxNum: An integer describing the number of chars in the sequence
            
        Returns:
            Nothing
        """
        super(seqGenerator, self).__init__()
        self._theSetOfChars = setOfChars
        self._maxNum = maxNum

    def __call__(self):
        return rstr.rstr(self._theSetOfChars, self._maxNum)


class dateGenerator(randomDataGenerator):
    """Defines a generator that returns a random date between two dates
    
    The class relies on the datetime module (https://docs.python.org/2/library/datetime.html)
    to generate a random valid date.
    
    Examples:
        P = dateGenerator(datetime.datetime.now()-datetime.timeinterval(weeks-4), datetime.datetime.now())
        
        This will generate a date within the last month.
        
    WARNING!!!
        At the moment no validation is performed on the datetime objects passed to this function
    """

    def __init__(self, dateStart, dateEnd):
        """Instantiates the random date generator with a start and end date
        
        Args:
            dateStart, dateEnd: datetime objects describing the date interval
        Returns:
            Nothing
        """
        super(dateGenerator, self).__init__()
        d = dateEnd - dateStart
        self._startDate = dateStart
        self._dateDiffSeconds = d.days * 86400 + d.seconds

    def __call__(self):
        return str(self._startDate + datetime.timedelta(seconds=random.
            randrange(self._dateDiffSeconds)))

<mask token>
import uuid
import random
import rstr
import datetime
from bunch import Bunch


class randomDataGenerator(object):
    """Base class for data generators
    
    Defines an abstract randomDataGenerator along with its basic data
    and interactions.
    
    A randomDataGenerator is supposed to define a model for the possible
    values generated by the randomDataGenerator.
    
    A randomDataGenerator is NOT supposed to define the values of instances.
    
    Consequently, randomDataGenerators have been modeled as callables. 
    They are instantiated to their internal state and produce an instance
    when they are called.
    
    Please note, this class is not supposed to be initialised directly.
    
    randomDataGenerators can be combined using three operators:
    * (mul),| (logic OR),^ (logic XOR) with the following semantics:
    
    (*) MUL: Cartesian Product
        P = optionGenerator(["Black", "White"])
        Q = optionGenerator(["Dwarf", "Hole"])
        K = P * Q

    (^) XOR: Chained evaluation with equal probabilities
        P = optionGenerator(["Black", "White"])
        Q = optionGenerator(["Hole", "Dwarf"])
        K = P^Q
        
        Upon successive calls, K will be returning "Black, White, Hole, Dwarf" as if it were
        one big optionGenerator(["Black", "White", "Hole", "Dwarf"])
        
    (|) OR: Conditional probability
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.
        
    Standard generators provided by DGen will have well defined semantics
    and combinations via operators (algebra). Derived generators are expected
    to redefine operators if required.
    """
    _name = None

    def __init__(self):
        """Standard constructor
        """
        self._name = None

    @property
    def name(self):
        """Returns the name of the generator
        """
        return self._name

    def setVarName(self, theName):
        """Sets the name of the generator
        
        Args:
            theName: A string naming the generator.
            There are no particular specifications for naming variables
            but given that the intention is to use the name to identify
            the variable at serialisation, a name that would not cause 
            problems if it were to be used as an attribute should be selected.
            
        Returns:
            The randomDataGenerator object itself. 
            This is to enable chained application of functions.
        """
        self._name = theName
        return self

    def __mul__(self, other):
        """Instantiates a composite generator as the cartesian product of two others
        
        Args:
            other: A randomDataGenerator being combined with this instance
            
        Returns:
            A compositeORGenerator that produces the cartesian product
            of two simple generators.
        """
        if isinstance(other, basestring):
            other = optionGenerator([other])
        return compositeORGenerator(left=self, right=other)

    def __or__(self, other):
        """Instantiates a conditional generator
        
        Similarly to __mul__ (*), this operator chains together two 
        randomDataGenerators in a specific way. The intention of overiding 
        the or operator (|) is to match the way conditional probabilities
        are expressed in mathematics (please see example further below).
        The consequence of this is that *this* generator, should be a
        condProbOptionGenerator. Otherwise, this combination is undefined.
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            A compositeConditionalGenerator implementing the intended semantics.
        """
        return compositeConditionalGenerator(left=self, right=other)

    def __xor__(self, other):
        """Instantiates an exclusive OR, randomDataGenerator
        
        This is essentially a convenience operator to package a series of 
        randomDataGenerators to one big optionGenerator.
        Please note, this is different than the __mul__ behaviour
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            An optionGenerator implementing the intended semantics.
        """
        return optionGenerator([(0.5, self), (0.5, other)])


class compositeGenerator(randomDataGenerator):
    """Defines a composite (binary) generator. A generator whose output depends on two unary ones.
    
    A composite generator maintains an internal "list of pointers" to other generators
    and it evaluates them in an appropriate way as dictated by the algebra (i.e. either in 
    conjunction or conditional mode)
    
    Please note, this class is not supposed to be initialised directly.
    """

    def __init__(self, left=None, right=None):
        """Instantiates the binary compositeGenerator as the result of two others (the left and right
        in terms of parsing)
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeGenerator
        """
        super(compositeGenerator, self).__init__()
        self._left = left
        self._right = right


class compositeORGenerator(compositeGenerator):
    """Defines the cartesian product of two randomDataGenerators
    """

    def __init__(self, left, right):
        """Instantiates the binary compositeORGenerator that implements the cartesian product of two randomDataGenerators
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        super(compositeORGenerator, self).__init__()
        self._left = left
        self._right = right

    def __mul__(self, other):
        """Extends the cartesian product generator.
        
        This is essentially an extension to the compositeORgenerator that defines
        what happens when compositeORGenerators are combined with each other.
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        return compositeORGenerator(left=self, right=other)

    def __call__(self):
        """Produces the cartesian product of the result of two unary generators.
        
        Args:
            None
        
        Returns:
            String
        """
        return self._left() + self._right()


class compositeConditionalGenerator(compositeGenerator):
    """Defines a composite generator that implements conditional evaluation
    of randomDataGenerators."""

    def __init__(self, left=None, right=None):
        """Instantiates the binary compositeConditionalGenerator that implements 
        conditional evaluation
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        super(compositeConditionalGenerator, self).__init__()
        self._left = left
        self._right = right

    def __or__(self, other):
        """Defines the behaviour of chaining conditionals with the OR operator (|)
        
        This is essentially, the value of **this** generator given the value of the other generator.
        
        Args:
            other: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        return compositeConditionalGenerator(left=self, right=other)

    def __call__(self):
        return self._left(self._right())


class constantGenerator(randomDataGenerator):
    """Defines a generator that simply returns a literal
    
    Constant generators are simply wrappers around literals so that they
    can be handled as randomDataGenerators too in expressions.
    """

    def __init__(self, theConstant):
        """Instantiates a constantGenerator
        
        Args:
            theConstant: A literal (e.g. a string)
        
        Returns:
            A constantGenerator
        """
        super(constantGenerator, self).__init__()
        self._theConstant = theConstant

    def __call__(self):
        return self._theConstant


class optionGenerator(randomDataGenerator):
    """Defines a random generator that produces "events" from a list of possible events
    
    An optionGenerator can be instantiated to an equal or varying probability events.
    
    Examples:
        P = optionGenerator(["Male", "Female"]) #P produces strings "Male", "Female" with equal probabilities
        P = optionGenerator([(0.2, "Male"), (0.8, "Female")]) #P produces strings "Male", "Female" with varying probabilities
    
    WARNING!
        At the moment, the varying probability flavour of optionGenerator does not check if probabilities of all events add up to one!
    
    optionGenerators can also be "chained" to call other optionGenerators creating more complex generators. For example:
    
        P = optionGenerator([optionGenerator("Black", "White"), optionGenerator("42", "3.1415928")])
        #Each event is produced with equal probabilities (e.g. 0.5*0.5=0.25)
        P = optionGenerator([(0.2,optionGenerator("Black", "White")),(0.8,optionGenerator("42","3.1415928"))])
        #Each event is produced with 0.2*0.5 and 0.8*0.5 probabilities.
    """

    def __init__(self, theOptions):
        """Instantiates the option generator
        
        Args:
            theOptions: A list of events or a tuple of (prob, event) pairs.
                        An "event" is either a literal (e.g. a string) or an optionGenerator
        
        Returns:
            Nothing
        """
        super(optionGenerator, self).__init__()
        self._Noptions = len(theOptions)
        if not self._Noptions:
            self._options = []
            return
        if not isinstance(theOptions[0], tuple):
            theOptions = [(1.0 / self._Noptions, x) for x in theOptions]
        theOptions = [(x[0], x[1] if not isinstance(x[1], basestring) else
            constantGenerator(x[1])) for x in theOptions]
        self._options = theOptions

    def __call__(self):
        """Evaluates the optionGenerator.
        
        Picks an event from the list of events proportional to its probability of appearance.
        """
        v = random.random()
        k = 0
        while v >= self._options[k][0]:
            v -= self._options[k][0]
            k += 1
        return self._options[k][1]()


class condProbOptionGenerator(compositeConditionalGenerator):
    """Defines a conditional probability generator
    
    A conditional probability generator enables the creation of complex "tree-like"
    evaluations of other randomDataGenerators.
    
    Example:
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.

    Please note:
        This is the only elementary data generator, whose evaluation
        requires a parameter. Using the (|) operator, this is hidden away
        but if required to be evaluated directly, then simply "calling"
        the generator will not work.
    """

    def __init__(self, theOptions):
        """Instantiates the condProbGenerator
        
        Args:
            theOptions: A dict of key:event, value:randomDataGenerator
            
        Returns:
            Nothing
        """
        super(condProbOptionGenerator, self).__init__()
        self._options = theOptions
        self._Noptions = len(self._options)

    def __call__(self, givenEvent):
        """Evaluates this randomDataGenerator GIVEN the value of givenEvent.
        
        Args:
            givenEvent: The event that drives the generation of instances from this generator.
            
        Returns:
            The evaluation of a generator, given givenEvent
        
        Please note:
            This is the only elementary data generator, whose evaluation
            requires a parameter.
        """
        return self._options[givenEvent]()


class archivedOptionGenerator(optionGenerator):
    """Defines an option generator that is instantiated by a file
    
    This is a very simple convenience class that essentially instantiates an optionGenerator with 
    data read from a file. Consequently, only strings (and probabilities of strings) can be described.
    
    Examples:
        Given a "data.csv" file:
        P = archivedOptionGenerator("data.csv")
        
        The format of the CSV file is either a single column of strings (each string in its own row)
        or pairs of prob,string (each pair in its own row).
        
        Lines begining with # are ignored (Taken as comments)
    """

    def __init__(self, aFilename):
        """Instantiates the generator with a file from the disk"""
        super(archivedOptionGenerator, self).__init__([])
        self._fromFile = aFilename
        fd = open(aFilename, 'r')
        self._options = filter(lambda x: not x.startswith('#'), map(lambda
            x: x[0:-1], fd.readlines()))
        fd.close()
        self._Noptions = len(self._options)
        if self._options[0].find(',') >= 0:
            newOptions = []
            for anOption in self._options:
                splitOption = anOption.split(',')
                newOptions.append((float(splitOption[0]), splitOption[1].
                    replace('\n', '') if not isinstance(splitOption[1],
                    basestring) else constantGenerator(splitOption[1].
                    replace('\n', ''))))
            self._options = newOptions
        else:
            self._options = map(lambda x: (1.0 / self._Noptions,
                constantGenerator(x.replace('\n', ''))), self._options)


class revRegexGenerator(randomDataGenerator):
    """Defines a reverse regular expression generator
    
    A reverse regular expression generator simply takes the definition of 
    the string and produces all strings that match it.
    
    This class relies on the excellent rstr module (https://pypi.python.org/pypi/rstr/2.1.3) for its output.
    
    Examples:
        P = revRegexGenerator("[0-9A-F][0-9A-f][0-9A-F][0-9A-F]")
        
        Upon successive calls to P, a new 4 digit hexadecial number will be
        produced.
        
    WARNING!!!
        If you P = revRegexGenerator("[0-9]*"), you are on your own.
    """

    def __init__(self, revRegex):
        """Instantiates the generator with a regex that is used to generate the string
        
        Args:
            revRegex: A string with the regular expression definition
        
        Returns:
            Nothing
        """
        super(revRegexGenerator, self).__init__()
        self._xeger = revRegex

    def __call__(self):
        """Evaluates the output of the generator"""
        return rstr.xeger(self._xeger)


class uidGenerator(randomDataGenerator):
    """Defines a randomDataGenerator that returns Universal Unique IDentifiers (UUID)
    
    This class relies on the standard uuid module (https://pypi.python.org/pypi/rstr/2.1.3) 
    which is included in the standard distribution of Python since version 2.5
    
    Examples:
        P = uidGenerator()
        
    """

    def __init__(self):
        """Instantiates the UUID generator"""
        super(uidGenerator, self).__init__()

    def __call__(self):
        return str(uuid.uuid4())


class seqGenerator(randomDataGenerator):
    """Defines a randomDataGenerator to generate sequences of characters
    
    This is essentially a "helper" class to generate random sequences of characters
    that again relies heavily on rstr (please also see documentation of revRegexGenerator).
    
    Examples:
        P = seqGenerator("ABCDEFGHIJKLMNOPQRST", maxNum=12)
        
        This will generate sequences of A-T characters which are 12 characters long.
    """

    def __init__(self, setOfChars, maxNum=1):
        """Instantiates the seqGenerator
        
        Args:
            setOfChars: A string with all the possible characters in the sequence
            maxNum: An integer describing the number of chars in the sequence
            
        Returns:
            Nothing
        """
        super(seqGenerator, self).__init__()
        self._theSetOfChars = setOfChars
        self._maxNum = maxNum

    def __call__(self):
        return rstr.rstr(self._theSetOfChars, self._maxNum)


class dateGenerator(randomDataGenerator):
    """Defines a generator that returns a random date between two dates
    
    The class relies on the datetime module (https://docs.python.org/2/library/datetime.html)
    to generate a random valid date.
    
    Examples:
        P = dateGenerator(datetime.datetime.now()-datetime.timeinterval(weeks-4), datetime.datetime.now())
        
        This will generate a date within the last month.
        
    WARNING!!!
        At the moment no validation is performed on the datetime objects passed to this function
    """

    def __init__(self, dateStart, dateEnd):
        """Instantiates the random date generator with a start and end date
        
        Args:
            dateStart, dateEnd: datetime objects describing the date interval
        Returns:
            Nothing
        """
        super(dateGenerator, self).__init__()
        d = dateEnd - dateStart
        self._startDate = dateStart
        self._dateDiffSeconds = d.days * 86400 + d.seconds

    def __call__(self):
        return str(self._startDate + datetime.timedelta(seconds=random.
            randrange(self._dateDiffSeconds)))

"""
Defines elementary generators

The module gathers together a set of operators that can be combined to 
generate random data values that conform to specifications that are
completely controlled by users

Notes:
 ADDITIONAL GENERATORS
 Link a generator to a distribution
   A number can be integer or float
   A distribution can have a number of different parameters
    rangeGenerator (int, float)

 Maybe it would be useful to be able to return the actual data type 
 that generates the "string" representation so that the data is still 
 computable for intermediate processing steps.
 
 Time series can be stationary or dynamic. Pregnancy rates could be 
 falling (for example) but this generator would generate births at 
 fixed rates.
 
Athanasios Anastasiou Sept 2016
"""

#TODO: Must also create something like Data Serialisers for various formats.

import uuid
import random
import rstr
import datetime
from bunch import Bunch

class randomDataGenerator(object):
    """Base class for data generators
    
    Defines an abstract randomDataGenerator along with its basic data
    and interactions.
    
    A randomDataGenerator is supposed to define a model for the possible
    values generated by the randomDataGenerator.
    
    A randomDataGenerator is NOT supposed to define the values of instances.
    
    Consequently, randomDataGenerators have been modeled as callables. 
    They are instantiated to their internal state and produce an instance
    when they are called.
    
    Please note, this class is not supposed to be initialised directly.
    
    randomDataGenerators can be combined using three operators:
    * (mul),| (logic OR),^ (logic XOR) with the following semantics:
    
    (*) MUL: Cartesian Product
        P = optionGenerator(["Black", "White"])
        Q = optionGenerator(["Dwarf", "Hole"])
        K = P * Q

    (^) XOR: Chained evaluation with equal probabilities
        P = optionGenerator(["Black", "White"])
        Q = optionGenerator(["Hole", "Dwarf"])
        K = P^Q
        
        Upon successive calls, K will be returning "Black, White, Hole, Dwarf" as if it were
        one big optionGenerator(["Black", "White", "Hole", "Dwarf"])
        
    (|) OR: Conditional probability
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.
        
    Standard generators provided by DGen will have well defined semantics
    and combinations via operators (algebra). Derived generators are expected
    to redefine operators if required.
    """
    _name = None
    
    def __init__(self):
        """Standard constructor
        """
        self._name = None
    
    @property
    def name(self):
        """Returns the name of the generator
        """
        return self._name
  
       
    def setVarName(self, theName):
        """Sets the name of the generator
        
        Args:
            theName: A string naming the generator.
            There are no particular specifications for naming variables
            but given that the intention is to use the name to identify
            the variable at serialisation, a name that would not cause 
            problems if it were to be used as an attribute should be selected.
            
        Returns:
            The randomDataGenerator object itself. 
            This is to enable chained application of functions.
        """
        self._name = theName
        return self
        
    def __mul__(self, other):        
        """Instantiates a composite generator as the cartesian product of two others
        
        Args:
            other: A randomDataGenerator being combined with this instance
            
        Returns:
            A compositeORGenerator that produces the cartesian product
            of two simple generators.
        """
        if isinstance(other, basestring):
            #TODO: Any literal should be converted to a constantGenerator for consistency
            other = optionGenerator([other])            
        return compositeORGenerator(left=self, right=other)
        
    def __or__(self,other):
        """Instantiates a conditional generator
        
        Similarly to __mul__ (*), this operator chains together two 
        randomDataGenerators in a specific way. The intention of overiding 
        the or operator (|) is to match the way conditional probabilities
        are expressed in mathematics (please see example further below).
        The consequence of this is that *this* generator, should be a
        condProbOptionGenerator. Otherwise, this combination is undefined.
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            A compositeConditionalGenerator implementing the intended semantics.
        """
        #TODO: Operators should raise an exception if the combination attempted does not make sense.
        return compositeConditionalGenerator(left=self, right=other)
    
    def __xor__(self, other):
        """Instantiates an exclusive OR, randomDataGenerator
        
        This is essentially a convenience operator to package a series of 
        randomDataGenerators to one big optionGenerator.
        Please note, this is different than the __mul__ behaviour
        
        Args:
            other: A randomDataGenerator being combined with this instance
        
        Returns:
            An optionGenerator implementing the intended semantics.
        """
        #TODO: Implement l*str, l*randomDataGenerator to mean probability*generator
        return optionGenerator([(0.5,self), (0.5,other)])

class compositeGenerator(randomDataGenerator):    
    """Defines a composite (binary) generator. A generator whose output depends on two unary ones.
    
    A composite generator maintains an internal "list of pointers" to other generators
    and it evaluates them in an appropriate way as dictated by the algebra (i.e. either in 
    conjunction or conditional mode)
    
    Please note, this class is not supposed to be initialised directly.
    """
       
    def __init__(self, left = None, right = None):
        """Instantiates the binary compositeGenerator as the result of two others (the left and right
        in terms of parsing)
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeGenerator
        """
        
        super(compositeGenerator,self).__init__()
        self._left = left
        self._right = right
             
class compositeORGenerator(compositeGenerator):
    """Defines the cartesian product of two randomDataGenerators
    """
    
    def __init__(self, left, right):        
        """Instantiates the binary compositeORGenerator that implements the cartesian product of two randomDataGenerators
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        super(compositeORGenerator,self).__init__()
        self._left = left
        self._right = right
        
    def __mul__(self,other):
        """Extends the cartesian product generator.
        
        This is essentially an extension to the compositeORgenerator that defines
        what happens when compositeORGenerators are combined with each other.
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeORGenerator
        """
        return compositeORGenerator(left = self, right = other)
    
    def __call__(self):
        """Produces the cartesian product of the result of two unary generators.
        
        Args:
            None
        
        Returns:
            String
        """
        return self._left() + self._right()
            
class compositeConditionalGenerator(compositeGenerator):
    """Defines a composite generator that implements conditional evaluation
    of randomDataGenerators."""    
    
    def __init__(self, left = None, right = None):
        """Instantiates the binary compositeConditionalGenerator that implements 
        conditional evaluation
        
        Args:
            left, right: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        super(compositeConditionalGenerator,self).__init__()
        self._left = left
        self._right = right
        
    def __or__(self,other):
        """Defines the behaviour of chaining conditionals with the OR operator (|)
        
        This is essentially, the value of **this** generator given the value of the other generator.
        
        Args:
            other: randomDataGenerators
        
        Returns:
            A compositeConditionalGenerator
        """
        #TODO: ensure that the "left" operand is a conditional itself.
        return compositeConditionalGenerator(left = self, right = other)
        
    def __call__(self):
        return self._left(self._right())
    
class constantGenerator(randomDataGenerator):
    """Defines a generator that simply returns a literal
    
    Constant generators are simply wrappers around literals so that they
    can be handled as randomDataGenerators too in expressions.
    """
    def __init__(self, theConstant):
        """Instantiates a constantGenerator
        
        Args:
            theConstant: A literal (e.g. a string)
        
        Returns:
            A constantGenerator
        """
        super(constantGenerator,self).__init__()
        self._theConstant = theConstant
        
    def __call__(self):
        return self._theConstant
        
class optionGenerator(randomDataGenerator):
    """Defines a random generator that produces "events" from a list of possible events
    
    An optionGenerator can be instantiated to an equal or varying probability events.
    
    Examples:
        P = optionGenerator(["Male", "Female"]) #P produces strings "Male", "Female" with equal probabilities
        P = optionGenerator([(0.2, "Male"), (0.8, "Female")]) #P produces strings "Male", "Female" with varying probabilities
    
    WARNING!
        At the moment, the varying probability flavour of optionGenerator does not check if probabilities of all events add up to one!
    
    optionGenerators can also be "chained" to call other optionGenerators creating more complex generators. For example:
    
        P = optionGenerator([optionGenerator("Black", "White"), optionGenerator("42", "3.1415928")])
        #Each event is produced with equal probabilities (e.g. 0.5*0.5=0.25)
        P = optionGenerator([(0.2,optionGenerator("Black", "White")),(0.8,optionGenerator("42","3.1415928"))])
        #Each event is produced with 0.2*0.5 and 0.8*0.5 probabilities.
    """
    
    def __init__(self, theOptions):
        """Instantiates the option generator
        
        Args:
            theOptions: A list of events or a tuple of (prob, event) pairs.
                        An "event" is either a literal (e.g. a string) or an optionGenerator
        
        Returns:
            Nothing
        """
        #TODO: Instead of basestring, check for number literals as well (or rather, anything that is NOT a randomDataGenerator needs to be wraped in one
        #TODO: Sum the probabilities to make sure they add up to 1 or throw an exception if they don't
        super(optionGenerator,self).__init__()
        self._Noptions = len(theOptions)        
        if not self._Noptions:
            self._options = []
            return
        #If this is not a list of tuples then it is assumed that each event is assigned an equal probability        
        if not isinstance(theOptions[0],tuple):
            theOptions = [(1.0 / self._Noptions, x) for x in theOptions]
        #Now make sure that if you have single strings appearing they are converted to generators internally
        theOptions = [(x[0], x[1] if not isinstance(x[1],basestring) else constantGenerator(x[1])) for x in theOptions]            
        self._options = theOptions        
    
    def __call__(self):
        """Evaluates the optionGenerator.
        
        Picks an event from the list of events proportional to its probability of appearance.
        """
        v = random.random()
        k=0
        while v>=self._options[k][0]:
            v-=self._options[k][0]
            k+=1
        return self._options[k][1]()
          
class condProbOptionGenerator(compositeConditionalGenerator):
    """Defines a conditional probability generator
    
    A conditional probability generator enables the creation of complex "tree-like"
    evaluations of other randomDataGenerators.
    
    Example:
        P = optionGenerator(["Black", "White"])
        Q = condProbOptionGenerator({"Black":optionGenerator("Mamba", "Hole"), "White":optionGenerator("Dwarf", "Stripes"})
        K = Q|P
        
        Upon successive calls, K will be returning Q **given** P. So, if 
        P was "Black", then only "Mamba" or "Hole" are produced but if P was 
        "White" then only "Dward" or "Stripes" are produced.

    Please note:
        This is the only elementary data generator, whose evaluation
        requires a parameter. Using the (|) operator, this is hidden away
        but if required to be evaluated directly, then simply "calling"
        the generator will not work.
    """
    def __init__(self,theOptions):
        """Instantiates the condProbGenerator
        
        Args:
            theOptions: A dict of key:event, value:randomDataGenerator
            
        Returns:
            Nothing
        """
        super(condProbOptionGenerator,self).__init__()
        self._options = theOptions
        self._Noptions = len(self._options)
        
    def __call__(self, givenEvent):
        """Evaluates this randomDataGenerator GIVEN the value of givenEvent.
        
        Args:
            givenEvent: The event that drives the generation of instances from this generator.
            
        Returns:
            The evaluation of a generator, given givenEvent
        
        Please note:
            This is the only elementary data generator, whose evaluation
            requires a parameter.
        """
        return self._options[givenEvent]()
            

class archivedOptionGenerator(optionGenerator):
    """Defines an option generator that is instantiated by a file
    
    This is a very simple convenience class that essentially instantiates an optionGenerator with 
    data read from a file. Consequently, only strings (and probabilities of strings) can be described.
    
    Examples:
        Given a "data.csv" file:
        P = archivedOptionGenerator("data.csv")
        
        The format of the CSV file is either a single column of strings (each string in its own row)
        or pairs of prob,string (each pair in its own row).
        
        Lines begining with # are ignored (Taken as comments)
    """
    def __init__(self,aFilename):        
        """Instantiates the generator with a file from the disk"""
        super(archivedOptionGenerator,self).__init__([])
        self._fromFile = aFilename
        fd = open(aFilename,"r")
        self._options = filter(lambda x:not x.startswith("#"),map(lambda x:x[0:-1],fd.readlines()))
        fd.close()
        self._Noptions = len(self._options)       
        #If there is a coma on the first line, it is assumed that all following lines contain a coma and that the first number is the probability of the event hapenning
        if self._options[0].find(",")>=0:
            newOptions = []
            for anOption in self._options:
                splitOption = anOption.split(",")
                newOptions.append((float(splitOption[0]),splitOption[1].replace("\n","") if not isinstance(splitOption[1],basestring) else constantGenerator(splitOption[1].replace("\n",""))))
            self._options = newOptions
        else:
            self._options = map(lambda x:(1.0 / self._Noptions, constantGenerator(x.replace("\n",""))),self._options)
            
class revRegexGenerator(randomDataGenerator):
    """Defines a reverse regular expression generator
    
    A reverse regular expression generator simply takes the definition of 
    the string and produces all strings that match it.
    
    This class relies on the excellent rstr module (https://pypi.python.org/pypi/rstr/2.1.3) for its output.
    
    Examples:
        P = revRegexGenerator("[0-9A-F][0-9A-f][0-9A-F][0-9A-F]")
        
        Upon successive calls to P, a new 4 digit hexadecial number will be
        produced.
        
    WARNING!!!
        If you P = revRegexGenerator("[0-9]*"), you are on your own.
    """
    def __init__(self,revRegex):
        """Instantiates the generator with a regex that is used to generate the string
        
        Args:
            revRegex: A string with the regular expression definition
        
        Returns:
            Nothing
        """
        super(revRegexGenerator,self).__init__()
        self._xeger = revRegex
        
    def __call__(self):
        """Evaluates the output of the generator"""
        return rstr.xeger(self._xeger)
        
class uidGenerator(randomDataGenerator):
    """Defines a randomDataGenerator that returns Universal Unique IDentifiers (UUID)
    
    This class relies on the standard uuid module (https://pypi.python.org/pypi/rstr/2.1.3) 
    which is included in the standard distribution of Python since version 2.5
    
    Examples:
        P = uidGenerator()
        
    """
    def __init__(self):
        """Instantiates the UUID generator"""
        super(uidGenerator,self).__init__()
        
    def __call__(self):
        return str(uuid.uuid4())
        
class seqGenerator(randomDataGenerator):
    """Defines a randomDataGenerator to generate sequences of characters
    
    This is essentially a "helper" class to generate random sequences of characters
    that again relies heavily on rstr (please also see documentation of revRegexGenerator).
    
    Examples:
        P = seqGenerator("ABCDEFGHIJKLMNOPQRST", maxNum=12)
        
        This will generate sequences of A-T characters which are 12 characters long.
    """
    def __init__(self, setOfChars, maxNum=1):
        """Instantiates the seqGenerator
        
        Args:
            setOfChars: A string with all the possible characters in the sequence
            maxNum: An integer describing the number of chars in the sequence
            
        Returns:
            Nothing
        """
        super(seqGenerator,self).__init__()
        self._theSetOfChars = setOfChars
        self._maxNum = maxNum
        
    def __call__(self):
        return rstr.rstr(self._theSetOfChars,self._maxNum)
        
        
class dateGenerator(randomDataGenerator):
    """Defines a generator that returns a random date between two dates
    
    The class relies on the datetime module (https://docs.python.org/2/library/datetime.html)
    to generate a random valid date.
    
    Examples:
        P = dateGenerator(datetime.datetime.now()-datetime.timeinterval(weeks-4), datetime.datetime.now())
        
        This will generate a date within the last month.
        
    WARNING!!!
        At the moment no validation is performed on the datetime objects passed to this function
    """
    def __init__(self, dateStart, dateEnd):
        """Instantiates the random date generator with a start and end date
        
        Args:
            dateStart, dateEnd: datetime objects describing the date interval
        Returns:
            Nothing
        """        
        #TODO: Raise an exception if dateEnd<dateStart.
        super(dateGenerator,self).__init__()
        d = dateEnd - dateStart
        self._startDate = dateStart
        self._dateDiffSeconds = d.days * 86400 + d.seconds        
                
    def __call__(self):
        return str(self._startDate + datetime.timedelta(seconds = random.randrange(self._dateDiffSeconds)))
