Blog: Random Username Generator: generate-username

File generate-username, 3.3 KB (added by retracile, 2 years ago)

generate-username

Line 
1#!/usr/bin/python3
2import os
3import sys
4import time
5import glob
6import random
7import math
8import argparse
9import subprocess
10
11
12TARGET_DIR = os.path.join(os.path.dirname(sys.argv[0]), 'wordlists')
13
14
15def clone_wordlists():
16    """Clone a copy of the wordlists if needed.  Update the wordlist if it
17    hasn't been in a while.
18    """
19    if os.path.exists(TARGET_DIR):
20        try:
21            last_modified = os.stat(os.path.join(TARGET_DIR, '.git/FETCH_HEAD')).st_mtime
22        except FileNotFoundError:
23            last_modified = 0
24        if last_modified < time.time() - 7 * 24 * 60 * 60: # Check for updates weekly
25            subprocess.check_call(['git', 'pull'], cwd=TARGET_DIR)
26    else:
27        subprocess.check_call(['git', 'clone', 'https://github.com/imsky/wordlists.git'],
28            cwd=os.path.dirname(TARGET_DIR))
29
30
31def load(part_of_speech):
32    """Load all unique words of a given part of speech from the wordlists.
33    """
34    words = set()
35    for filename in glob.glob(os.path.join(TARGET_DIR, part_of_speech, '*.txt')):
36        words.update(w.strip() for w in open(filename, encoding='utf-8') if w.strip())
37    return words
38
39
40def main(argv=None):
41    """Generate a random-but-memorable username.
42    """
43    if argv is None:
44        argv = sys.argv
45    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
46        description=main.__doc__)
47    parser.add_argument("--adjectives", "-a", default=1, type=int,
48                        help="number of adjectives")
49    parser.add_argument("--divider", default='',
50                        help="character or string between words")
51    parser.add_argument("--bits", default=0, type=int,
52                        help="minimum bits of entropy")
53    parser.add_argument("--verbose", action='store_true',
54                        help="be chatty")
55    args = parser.parse_args(argv[1:])
56
57    clone_wordlists()
58    # load() returns a set, but we need a subscriptable type, so convert to
59    # lists.  But there's no reason to sort them, since we're only taking
60    # random picks from the lists.
61    nouns = list(load('nouns'))
62    adjectives = list(load('adjectives'))
63
64    adjective_count = args.adjectives
65    # Determine the number of bits of randomness given the adjectives count
66    bits = math.log2(len(nouns)) + adjective_count * math.log2(len(adjectives))
67    # Increase the number of adjectives as needed to achieve the minimum bits
68    # of randomness
69    while bits < args.bits:
70        adjective_count += 1
71        bits = math.log2(len(nouns)) + adjective_count * math.log2(len(adjectives))
72
73    if args.verbose:
74        sys.stderr.write(f"Found {len(nouns)} nouns and {len(adjectives)} adjectives;"
75            f" using {adjective_count} adjective{adjective_count != 1 and 's' or ''} "
76            f"and one noun giving {bits :0.2f} bits of entropy.\n")
77
78    # Use the best source of randomness available.
79    prng = random.SystemRandom()
80    words = []
81    for i in range(adjective_count):
82        words.append(prng.choice(adjectives))
83        # We don't remove used adjectives from the list of choices, so that
84        # means it's possible to have repeats, such as "wise-small-wise-ant"
85    words.append(prng.choice(nouns))
86    result = args.divider.join(words)
87    sys.stdout.write(f"{result}\n")
88    return 0
89
90
91if __name__ == '__main__':
92    sys.exit(main())