Created
September 14, 2020 15:47
-
-
Save rogfrich/20ae98c02cae974b610732074f63dad3 to your computer and use it in GitHub Desktop.
My solution to exercise 37 in "Exercises for Programmers: 57 Challenges to Develop Your Coding Skills" by Brian P Hogan.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Exercises for Programmers: 57 Challenges to Develop Your Coding Skills. | |
Exercise 37: Password Generator. | |
Requirements: | |
- Create a program to generate a secure password. | |
- Prompt for length, number of special characters, and the number of numeric chars. | |
Constraints: | |
- Store characters used to generate passwords in lists. | |
- Add some randomness to the password generation. | |
Extra credit challenges: | |
- Randomly convert vowels to numbers such as 3 for E and 4 for A. | |
- Have the program present a few options for the user to choose from. | |
- Place the password on the user's clipboard when generated. | |
""" | |
import pyperclip | |
import secrets | |
import random # used in one specific place because secrets does not have an equivalent to random.shuffle() | |
import string | |
class PasswordGenerator: | |
def __init__(self, password_length=None, qty_special_characters=None, qty_numeric_characters=None): | |
# get user inputs, or receive them via the function call for testing purposes. | |
if not password_length: | |
self.password_length = self.get_length_from_user() | |
else: | |
self.password_length = password_length | |
if not qty_special_characters: | |
self.qty_special_chars = self.get_qty_special_chars_from_user() | |
else: | |
self.qty_special_chars = qty_special_characters | |
if not qty_numeric_characters: | |
self.qty_numeric_chars = self.get_qty_numeric_chars_from_user() | |
else: | |
self.qty_numeric_chars = qty_numeric_characters | |
self.generated_passwords = [] | |
self.vowel_replacements = { | |
"a": "4", | |
"e": "3", | |
"i": "1", | |
"o": "0", | |
"u": "9" | |
} | |
def get_length_from_user(self): | |
while True: | |
length = input("Enter the length of the password you want to create: > ") | |
if length.isnumeric(): | |
return int(length) | |
def get_qty_special_chars_from_user(self): | |
while True: | |
qty_special_characters = input("Enter the quantity of SPECIAL characters to include: > ") | |
if qty_special_characters.isnumeric() and int(qty_special_characters) < self.password_length: | |
return int(qty_special_characters) | |
def get_qty_numeric_chars_from_user(self): | |
while True: | |
qty_numeric_chars = input("Enter the quantity of NUMERIC characters to include: > ") | |
if qty_numeric_chars.isnumeric() and int(qty_numeric_chars) + self.qty_special_chars < self.password_length: | |
return int(qty_numeric_chars) | |
def generate_password(self): | |
pw = [] | |
# Randomly select numeric characters | |
for i in range(int(self.qty_numeric_chars)): | |
pw.append(str(secrets.randbelow(10))) | |
# Randomly select special characters | |
for i in range(int(self.qty_special_chars)): | |
pw.append(secrets.choice("!@#$%^&*()")) | |
# Randomly select alpha characters. 50% chance that a vowel will be converted to a number. | |
for i in range(int(self.password_length) - (int(self.qty_special_chars) + int(self.qty_numeric_chars))): | |
random_char = secrets.choice(string.ascii_letters) | |
if random_char.lower() in "aeiou" and secrets.choice([0, 1]) == 0: | |
pw.append(self.vowel_replacements[random_char.lower()]) | |
continue | |
pw.append(random_char) | |
assert len(pw) == self.password_length | |
random.shuffle(pw) | |
return ''.join(pw) | |
def main(): | |
QTY_OF_OPTIONS = 3 # The number of generated passwords presented to the user to choose from | |
pw = PasswordGenerator() | |
passwords = {} | |
for index, _ in enumerate(range(QTY_OF_OPTIONS)): | |
passwords[str(index)] = pw.generate_password() | |
print("\nPlease choose a password from the randomly generated list below:") | |
for k, v in passwords.items(): | |
print(f"{k}: {v})") | |
while True: | |
choice = input("> ") | |
if choice in passwords.keys(): | |
break | |
chosen_password = passwords[choice] | |
pyperclip.copy(chosen_password) | |
print(f"\"{chosen_password}\" copied to clipboard") | |
def tests(): | |
# Run "pytest main.py" from the terminal command line | |
pg = PasswordGenerator( | |
password_length=8, | |
qty_numeric_characters="1", | |
qty_special_characters="1", | |
) | |
pw = pg.generate_password() | |
print(pg.qty_numeric_chars) | |
# Test that length of generated password is correct: | |
assert len(pw) == 8 | |
# Test that there is only one special char in generated password: | |
number_of_special_chars = len([x for x in pw if x in "!@#$%^&*()"]) | |
assert number_of_special_chars == 1 | |
# Test that there are 1 < n <len(pw) numeric chars in pw (exact number is indeterminate because of random) | |
number_of_numeric_chars = len([x for x in pw if x.isnumeric()]) | |
assert 1 <= number_of_numeric_chars < len(pw) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment