Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# Source code for evennia.contrib.utils.random_string_generator.random_string_generator

    """
    Pseudo-random generator and registry

    Evennia contribution - Vincent Le Goff 2017

    This contrib can be used to generate pseudo-random strings of information
    with specific criteria.  You could, for instance, use it to generate
    phone numbers, license plate numbers, validation codes, non-sensivite
    passwords and so on.  The strings generated by the generator will be
    stored and won't be available again in order to avoid repetition.
    Here's a very simple example:

    ```python
    from evennia.contrib.utils.random_string_generator import RandomStringGenerator
    # Create a generator for phone numbers
    phone_generator = RandomStringGenerator("phone number", r"555-[0-9]{3}-[0-9]{4}")
    # Generate a phone number (555-XXX-XXXX with X as numbers)
    number = phone_generator.get()
    # `number` will contain something like: "555-981-2207"
    # If you call `phone_generator.get`, it won't give the same anymore.phone_generator.all()
    # Will return a list of all currently-used phone numbers
    phone_generator.remove("555-981-2207")
    # The number can be generated again
    ```

    To use it, you will need to:

    1. Import the `RandomStringGenerator` class from the contrib.
    2. Create an instance of this class taking two arguments:
       - The name of the gemerator (like "phone number", "license plate"...).
       - The regular expression representing the expected results.
    3. Use the generator's `all`, `get` and `remove` methods as shown above.

    To understand how to read and create regular expressions, you can refer to
    [the documentation on the re module](https://docs.python.org/2/library/re.html).
    Some examples of regular expressions you could use:

    - `r"555-\d{3}-\d{4}"`: 555, a dash, 3 digits, another dash, 4 digits.
    - `r"[0-9]{3}[A-Z][0-9]{3}"`: 3 digits, a capital letter, 3 digits.
    - `r"[A-Za-z0-9]{8,15}"`: between 8 and 15 letters and digits.
    - ...

    Behind the scenes, a script is created to store the generated information
    for a single generator.  The `RandomStringGenerator` object will also
    read the regular expression you give to it to see what information is
    required (letters, digits, a more restricted class, simple characters...)...
    More complex regular expressions (with branches for instance) might not be
    available.

    """

    import re
    import string
    import time
    from random import choice, randint, seed

    from evennia import DefaultScript, ScriptDB
    from evennia.utils.create import create_script

    class RejectedRegex(RuntimeError):

        """The provided regular expression has been rejected.

        More details regarding why this error occurred will be provided in
        the message.  The usual reason is the provided regular expression is
        not specific enough and could lead to inconsistent generating.

        """

        pass

    class ExhaustedGenerator(RuntimeError):

        """The generator hasn't any available strings to generate anymore."""

        pass

    class RandomStringGeneratorScript(DefaultScript):

        """
        The global script to hold all generators.

        It will be automatically created the first time `generate` is called
        on a RandomStringGenerator object.

        """

        def at_script_creation(self):
            """Hook called when the script is created."""
            self.key = "generator_script"
            self.desc = "Global generator script"
            self.persistent = True

            # Permanent data to be stored
            self.db.generated = {}

    class RandomStringGenerator:

        """
        A generator class to generate pseudo-random strings with a rule.

        The "rule" defining what the generator should provide in terms of
        string is given as a regular expression when creating instances of
        this class.  You can use the `all` method to get all generated strings,
        the `get` method to generate a new string, the `remove` method
        to remove a generated string, or the `clear` method to remove all
        generated strings.

        Bear in mind, however, that while the generated strings will be
        stored to avoid repetition, the generator will not concern itself
        with how the string is stored on the object you use.  You probably
        want to create a tag to mark this object.  This is outside of the scope
        of this class.

        """

        # We keep the script as a class variable to optimize querying
        # with multiple instandces
        script = None

        def __init__(self, name, regex):
            """
            Create a new generator.

            Args:
                name (str): name of the generator to create.
                regex (str): regular expression describing the generator.

            Notes:
                `name` should be an explicit name.  If you use more than one
                generator in your game, be sure to give them different names.
                This name will be used to store the generated information
                in the global script, and in case of errors.

                The regular expression should describe the generator, what
                it should generate: a phone number, a license plate, a password
                or something else.  Regular expressions allow you to use
                pretty advanced criteria, but be aware that some regular
                expressions will be rejected if not specific enough.

            Raises:
                RejectedRegex: the provided regular expression couldn't be
                accepted as a valid generator description.

            """
            self.name = name
            self.elements = []
            self.total = 1

            # Analyze the regex if any
            if regex:
                self._find_elements(regex)

        def __repr__(self):
            return (
                "<evennia.contrib.utils.random_string_generator.RandomStringGenerator for {}>".format(
                    self.name
                )
            )

        def _get_script(self):
            """Get or create the script."""
            if type(self).script:
                return type(self).script

            try:
                script = ScriptDB.objects.get(db_key="generator_script")
            except ScriptDB.DoesNotExist:
                script = create_script(
                    "evennia.contrib.utils.random_string_generator.RandomStringGeneratorScript"
                )

            type(self).script = script
            return script

        def _find_elements(self, regex):
            """
            Find the elements described in the regular expression.  This will
            analyze the provided regular expression and try to find elements.

            Args:
                regex (str): the regular expression.

            """
            try:
                # python 3.11
                regex_parser = re._parser
            except AttributeError:
                # python <3.11
                regex_parser = re.sre_parse

            self.total = 1
            self.elements = []
            tree = regex_parser.parse(regex).data  # note - sre_parse removed in py3.11
            # `tree` contains a list of elements in the regular expression
            for element in tree:
                # `element` is also a list, the first element is a string
                name = str(element[0]).lower()
                desc = {"min": 1, "max": 1}

                # If `.`, break here
                if name == "any":
                    raise RejectedRegex(
                        "the . definition is too broad, specify what you need more precisely"
                    )
                elif name == "at":
                    # Either the beginning or end, we ignore it
                    continue
                elif name == "min_repeat":
                    raise RejectedRegex("you have to provide a maximum number of this character class")
                elif name == "max_repeat":
                    desc["min"] = element[1][0]
                    desc["max"] = element[1][1]
                    desc["chars"] = self._find_literal(element[1][2][0])
                elif name == "in":
                    desc["chars"] = self._find_literal(element)
                elif name == "literal":
                    desc["chars"] = self._find_literal(element)
                else:
                    raise RejectedRegex("unhandled regex syntax:: {}".format(repr(name)))

                self.elements.append(desc)
                self.total *= len(desc["chars"]) ** desc["max"]

        def _find_literal(self, element):
            """Find the literal corresponding to a piece of regular expression."""
            name = str(element[0]).lower()
            chars = []
            if name == "literal":
                chars.append(chr(element[1]))
            elif name == "in":
                negate = False
                if element[1][0][0] == "negate":
                    negate = True
                    chars = list(string.ascii_letters + string.digits)

                for part in element[1]:
                    if part[0] == "negate":
                        continue

                    sublist = self._find_literal(part)
                    for char in sublist:
                        if negate:
                            if char in chars:
                                chars.remove(char)
                        else:
                            chars.append(char)
            elif name == "range":
                chars = [chr(i) for i in range(element[1][0], element[1][1] + 1)]
            elif name == "category":
                category = str(element[1]).lower()
                if category == "category_digit":
                    chars = list(string.digits)
                elif category == "category_word":
                    chars = list(string.letters)
                else:
                    raise RejectedRegex("unknown category: {}".format(category))
            else:
                raise RejectedRegex("cannot find the literal: {}".format(element[0]))

            return chars

        def all(self):
            """
            Return all generated strings for this generator.

            Returns:
                strings (list of strr): the list of strings that are already
                used.  The strings that were generated first come first in the list.

            """
            script = self._get_script()
            generated = list(script.db.generated.get(self.name, []))
            return generated

        def get(self, store=True, unique=True):
            """
            Generate a pseudo-random string according to the regular expression.

            Args:
                store (bool, optional): store the generated string in the script.
                unique (bool, optional): keep on trying if the string is already used.

            Returns:
                The newly-generated string.

            Raises:
                ExhaustedGenerator: if there's no available string in this generator.

            Note:
                Unless asked explicitly, the returned string can't repeat itself.

            """
            script = self._get_script()
            generated = script.db.generated.get(self.name)
            if generated is None:
                script.db.generated[self.name] = []
                generated = script.db.generated[self.name]

            if len(generated) >= self.total:
                raise ExhaustedGenerator

            # Generate a pseudo-random string that might be used already
            result = ""
            for element in self.elements:
                number = randint(element["min"], element["max"])
                chars = element["chars"]
                for index in range(number):
                    char = choice(chars)
                    result += char

            # If the string has already been generated, try again
            if result in generated and unique:
                # Change the random seed, incrementing it slowly
                epoch = time.time()
                while result in generated:
                    epoch += 1
                    seed(epoch)
                    result = self.get(store=False, unique=False)

            if store:
                generated.append(result)

            return result

        def remove(self, element):
            """
            Remove a generated string from the list of stored strings.

            Args:
                element (str): the string to remove from the list of generated strings.

            Raises:
                ValueError: the specified value hasn't been generated and is not present.

            Note:
                The specified string has to be present in the script (so
                has to have been generated).  It will remove this entry
                from the script, so this string could be generated again by
                calling the `get` method.

            """
            script = self._get_script()
            generated = script.db.generated.get(self.name, [])
            if element not in generated:
                raise ValueError(
                    "the string {} isn't stored as generated by the generator {}".format(
                        element, self.name
                    )
                )

            generated.remove(element)

        def clear(self):
            """
            Clear the generator of all generated strings.

            """
            script = self._get_script()
            generated = script.db.generated.get(self.name, [])
            generated[:] = []

Note

You are reading an old version of the Evennia documentation. The latest version is here

.