-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Lorena Mesa
committed
Jul 17, 2024
1 parent
029d0b7
commit 937150e
Showing
4 changed files
with
192 additions
and
11 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Set up Python | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: '3.10' | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install -r requirements.txt | ||
- name: Test with unittest | ||
run: | | ||
python3 -m unittest test/sample_cases_test.py |
Empty file.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import argparse | ||
import hashlib | ||
from PIL import Image, ImageDraw | ||
|
||
__author__ = "Lorena Mesa" | ||
__email__ = "[email protected]" | ||
|
||
|
||
class Identicon: | ||
|
||
def __init__(self, input_str: str) -> None: | ||
self.md5hash_str: str = self._convert_string_to_sha_hash(input_str) | ||
|
||
def _convert_string_to_sha_hash(self, input_str: str) -> str: | ||
""" | ||
Function that takes an input string and returns a md5 hexdigest string. | ||
:return: md5 hexdigest of an input string | ||
""" | ||
if len(input_str) < 1: | ||
raise ValueError("Input string cannot be empty.") | ||
|
||
return hashlib.md5(input_str.encode('utf-8')).hexdigest() | ||
|
||
def _build_grid(self) -> list[list]: | ||
""" | ||
Function that takes an input md5 hexdigest string and builds | ||
a list of lists using grid size to determine the size of the | ||
grid. Each value within the list of lists contains a row of booleans | ||
that indicates if that given element will be filled with a color. | ||
:return: a list of lists representing a grid of the pixels to be drawn in a PIL Image | ||
""" | ||
grid_size: int = 5 | ||
grid: list = [] | ||
for row_number in range(grid_size): | ||
row: list = list() | ||
for element_number in range(grid_size): | ||
element: int = row_number * grid_size + element_number + 6 | ||
fill_element: bool = int(self.md5hash_str[element], base=16) % 2 == 0 | ||
row.append(fill_element) | ||
grid.append(row) | ||
return grid | ||
|
||
def _generate_image_fill_color(self, md5hash_str: str) -> tuple: | ||
""" | ||
Function that generates a R,G,B value to use to fill the PIL Image. | ||
:param md5hash_str: md5 hexdigest of an input string | ||
:return: a tuple of numbers representing the R,G.B value to fill the PIL Image | ||
""" | ||
return tuple(int(md5hash_str[i:i+2], base=16) for i in range(0, 2*3, 2)) | ||
|
||
def draw_image(self, filename: str=None) -> Image: | ||
""" | ||
Function that generates a grid - a list of lists - indicating which pixels are to be filled | ||
and uses the md5hash_str to generate an image fill color. Function creates a PIL Image, drawing it, | ||
and saving it. | ||
:param filename: filename of PIL png image generated | ||
:return: None | ||
""" | ||
|
||
fill_color: tuple = self._generate_image_fill_color(self.md5hash_str) | ||
grid: list[list] = self._build_grid() | ||
|
||
SQUARE: int = 50 | ||
size: tuple = (5 * 50, 5 * 50) | ||
bg_color: tuple = (214,214,214) | ||
|
||
image: Image = Image.new("RGB", size, bg_color) | ||
draw: ImageDraw = ImageDraw.Draw(image) | ||
|
||
# Makes the identicon symmetrical | ||
for i in range(5): | ||
grid[i][4] = grid[i][0] | ||
grid[i][3] = grid[i][1] | ||
|
||
for row in range(5): | ||
for element in range(5): | ||
# Boolean check to confirm 'True' to draw and fill the pixel in the iamge | ||
if grid[row][element]: | ||
bounding_box: list[int] = [element * SQUARE, row * SQUARE, element * SQUARE + SQUARE, row * SQUARE + SQUARE] | ||
# TODO: Should we use multiple fill colors? May need to draw multiple rectangles to obtain this | ||
draw.rectangle(bounding_box, fill=fill_color) | ||
|
||
if not filename: | ||
filename: str = 'example' | ||
|
||
# TODO: Confirm overwrite file is one of same name exists | ||
image.save(f'{filename}.png') | ||
|
||
if __name__ == '__main__': | ||
parser = argparse.ArgumentParser( | ||
description="Generate an identicon with Python 3.", | ||
usage="""Example: python main.py -s='931D387731bBbC988B31220' or add the optional -o flag to specify name of identicon | ||
image generated such as python main.py -s='931D387731bBbC988B31220' -o='my_identicon.jpg'.""" | ||
) | ||
|
||
def len_gt_zero(input_str: str): | ||
if len(input_str) > 0: | ||
return input_str | ||
raise argparse.ArgumentTypeError("Input string must have length greater than 0 in order to generate an identicon.") | ||
|
||
parser.add_argument( | ||
"-s", | ||
"--string", | ||
default="", | ||
type=str, | ||
required=True, | ||
help="An input string used to generate an identicon.", | ||
) | ||
parser.add_argument( | ||
"-o", | ||
"--output", | ||
default="", | ||
type=str, | ||
required=False, | ||
help="Name for output identicon image generated.", | ||
) | ||
|
||
args = parser.parse_args() | ||
|
||
identicon = Identicon(input_str=args.string) | ||
identicon.draw_image(filename=args.output) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from os import remove | ||
from pathlib import Path | ||
from PIL import Image | ||
from PIL import Image, PngImagePlugin | ||
import subprocess | ||
import unittest | ||
|
||
from main import Identicon | ||
from src.main import Identicon | ||
|
||
__author__ = "Lorena Mesa" | ||
__email__ = "[email protected]" | ||
|
@@ -14,23 +15,63 @@ | |
|
||
|
||
class TestUI(unittest.TestCase): | ||
def test_ui_fails_to_create_identicon_with_input_text_missing(self): | ||
def test_ui_fails_to_create_identicon_with_input_string_missing(self): | ||
with self.assertRaises(subprocess.CalledProcessError) as context: | ||
subprocess.check_output(f"python3 {PROJECT_ROOT}/main.py", shell=True, stderr=subprocess.STDOUT).strip() | ||
self.assertIn(context.exception.message, "main.py: error: the following arguments are required: -s/--string") | ||
subprocess.check_output(f"python3 {PROJECT_ROOT}/src/main.py", shell=True, stderr=subprocess.STDOUT).strip() | ||
self.assertIn("main.py: error: the following arguments are required: -s/--string", context.exception.output.decode('utf-8')) | ||
|
||
|
||
class TestHappyPath(unittest.TestCase): | ||
def test_successfully_creates_identicon(self): | ||
identicon = Identicon("931D387731bBbC988B31220") | ||
identicon.draw_image(filename="output") | ||
image = Image.open(f"{PROJECT_ROOT}/output.png", mode="r") | ||
self.assertIsInstance(image, Image, "Image created is not of type PIL.Image") | ||
generated_image = Image.open(f"{PROJECT_ROOT}/output.png", mode="r") | ||
self.assertIsInstance(generated_image, PngImagePlugin.PngImageFile) | ||
remove(f"{PROJECT_ROOT}/output.png") | ||
|
||
def test_successfully_creates_same_identicon_for_same_input_strings(self): | ||
# Make 1st identicon | ||
identicon_john_1 = Identicon("john") | ||
identicon_john_1.draw_image(filename="john1") | ||
# Make 2nd identicon | ||
identicon_john_2 = Identicon("john") | ||
identicon_john_2.draw_image(filename="john2") | ||
|
||
# Assertions | ||
generated_john_1 = Image.open(f"{PROJECT_ROOT}/john1.png", mode="r") | ||
self.assertIsInstance(generated_john_1, PngImagePlugin.PngImageFile) | ||
|
||
generated_john_2 = Image.open(f"{PROJECT_ROOT}/john2.png", mode="r") | ||
self.assertIsInstance(generated_john_2, PngImagePlugin.PngImageFile) | ||
|
||
self.assertEqual(generated_john_1, generated_john_2) | ||
|
||
# Cleanup | ||
remove(f"{PROJECT_ROOT}/john1.png") | ||
remove(f"{PROJECT_ROOT}/john2.png") | ||
|
||
def test_does_not_create_same_identicon_for_different_input_strings(self): | ||
# Make 1st identicon | ||
identicon_john = Identicon("john") | ||
identicon_john.draw_image(filename="john") | ||
# Make 2nd identicon | ||
identicon_john_2 = Identicon("jane") | ||
identicon_john_2.draw_image(filename="jane") | ||
|
||
# Assertions | ||
generated_john = Image.open(f"{PROJECT_ROOT}/john.png", mode="r") | ||
self.assertIsInstance(generated_john, PngImagePlugin.PngImageFile) | ||
|
||
generated_jane = Image.open(f"{PROJECT_ROOT}/jane.png", mode="r") | ||
self.assertIsInstance(generated_jane, PngImagePlugin.PngImageFile) | ||
|
||
self.assertNotEqual(generated_john, generated_jane) | ||
|
||
# Cleanup | ||
remove(f"{PROJECT_ROOT}/john.png") | ||
remove(f"{PROJECT_ROOT}/jane.png") | ||
|
||
|
||
# hash_str =convert_string_to_sha_hash("931D387731bBbC988B31220") | ||
# hash_str = convert_string_to_sha_hash("[email protected]") | ||
# grid = build_grid(hash_str) | ||
# draw_image(grid, hash_str) | ||
|
||
if __name__ == '__maipython -m unittest__': | ||
unittest.main() |