Skip to content

Commit 937150e

Browse files
author
Lorena Mesa
committed
Move project structure around
1 parent 029d0b7 commit 937150e

File tree

4 files changed

+192
-11
lines changed

4 files changed

+192
-11
lines changed
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
steps:
2+
- uses: actions/checkout@v4
3+
- name: Set up Python
4+
uses: actions/setup-python@v5
5+
with:
6+
python-version: '3.10'
7+
- name: Install dependencies
8+
run: |
9+
python -m pip install --upgrade pip
10+
pip install -r requirements.txt
11+
- name: Test with unittest
12+
run: |
13+
python3 -m unittest test/sample_cases_test.py

src/__init__.py

Whitespace-only changes.

src/main.py

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import hashlib
5+
from PIL import Image, ImageDraw
6+
7+
__author__ = "Lorena Mesa"
8+
__email__ = "[email protected]"
9+
10+
11+
class Identicon:
12+
13+
def __init__(self, input_str: str) -> None:
14+
self.md5hash_str: str = self._convert_string_to_sha_hash(input_str)
15+
16+
def _convert_string_to_sha_hash(self, input_str: str) -> str:
17+
"""
18+
Function that takes an input string and returns a md5 hexdigest string.
19+
20+
:return: md5 hexdigest of an input string
21+
"""
22+
if len(input_str) < 1:
23+
raise ValueError("Input string cannot be empty.")
24+
25+
return hashlib.md5(input_str.encode('utf-8')).hexdigest()
26+
27+
def _build_grid(self) -> list[list]:
28+
"""
29+
Function that takes an input md5 hexdigest string and builds
30+
a list of lists using grid size to determine the size of the
31+
grid. Each value within the list of lists contains a row of booleans
32+
that indicates if that given element will be filled with a color.
33+
34+
:return: a list of lists representing a grid of the pixels to be drawn in a PIL Image
35+
"""
36+
grid_size: int = 5
37+
grid: list = []
38+
for row_number in range(grid_size):
39+
row: list = list()
40+
for element_number in range(grid_size):
41+
element: int = row_number * grid_size + element_number + 6
42+
fill_element: bool = int(self.md5hash_str[element], base=16) % 2 == 0
43+
row.append(fill_element)
44+
grid.append(row)
45+
return grid
46+
47+
def _generate_image_fill_color(self, md5hash_str: str) -> tuple:
48+
"""
49+
Function that generates a R,G,B value to use to fill the PIL Image.
50+
51+
:param md5hash_str: md5 hexdigest of an input string
52+
:return: a tuple of numbers representing the R,G.B value to fill the PIL Image
53+
"""
54+
return tuple(int(md5hash_str[i:i+2], base=16) for i in range(0, 2*3, 2))
55+
56+
def draw_image(self, filename: str=None) -> Image:
57+
"""
58+
Function that generates a grid - a list of lists - indicating which pixels are to be filled
59+
and uses the md5hash_str to generate an image fill color. Function creates a PIL Image, drawing it,
60+
and saving it.
61+
62+
:param filename: filename of PIL png image generated
63+
:return: None
64+
"""
65+
66+
fill_color: tuple = self._generate_image_fill_color(self.md5hash_str)
67+
grid: list[list] = self._build_grid()
68+
69+
SQUARE: int = 50
70+
size: tuple = (5 * 50, 5 * 50)
71+
bg_color: tuple = (214,214,214)
72+
73+
image: Image = Image.new("RGB", size, bg_color)
74+
draw: ImageDraw = ImageDraw.Draw(image)
75+
76+
# Makes the identicon symmetrical
77+
for i in range(5):
78+
grid[i][4] = grid[i][0]
79+
grid[i][3] = grid[i][1]
80+
81+
for row in range(5):
82+
for element in range(5):
83+
# Boolean check to confirm 'True' to draw and fill the pixel in the iamge
84+
if grid[row][element]:
85+
bounding_box: list[int] = [element * SQUARE, row * SQUARE, element * SQUARE + SQUARE, row * SQUARE + SQUARE]
86+
# TODO: Should we use multiple fill colors? May need to draw multiple rectangles to obtain this
87+
draw.rectangle(bounding_box, fill=fill_color)
88+
89+
if not filename:
90+
filename: str = 'example'
91+
92+
# TODO: Confirm overwrite file is one of same name exists
93+
image.save(f'{filename}.png')
94+
95+
if __name__ == '__main__':
96+
parser = argparse.ArgumentParser(
97+
description="Generate an identicon with Python 3.",
98+
usage="""Example: python main.py -s='931D387731bBbC988B31220' or add the optional -o flag to specify name of identicon
99+
image generated such as python main.py -s='931D387731bBbC988B31220' -o='my_identicon.jpg'."""
100+
)
101+
102+
def len_gt_zero(input_str: str):
103+
if len(input_str) > 0:
104+
return input_str
105+
raise argparse.ArgumentTypeError("Input string must have length greater than 0 in order to generate an identicon.")
106+
107+
parser.add_argument(
108+
"-s",
109+
"--string",
110+
default="",
111+
type=str,
112+
required=True,
113+
help="An input string used to generate an identicon.",
114+
)
115+
parser.add_argument(
116+
"-o",
117+
"--output",
118+
default="",
119+
type=str,
120+
required=False,
121+
help="Name for output identicon image generated.",
122+
)
123+
124+
args = parser.parse_args()
125+
126+
identicon = Identicon(input_str=args.string)
127+
identicon.draw_image(filename=args.output)

test/sample_cases_test.py

+52-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
#!/usr/bin/env python3
22

3+
from os import remove
34
from pathlib import Path
4-
from PIL import Image
5+
from PIL import Image, PngImagePlugin
56
import subprocess
67
import unittest
78

8-
from main import Identicon
9+
from src.main import Identicon
910

1011
__author__ = "Lorena Mesa"
1112
__email__ = "[email protected]"
@@ -14,23 +15,63 @@
1415

1516

1617
class TestUI(unittest.TestCase):
17-
def test_ui_fails_to_create_identicon_with_input_text_missing(self):
18+
def test_ui_fails_to_create_identicon_with_input_string_missing(self):
1819
with self.assertRaises(subprocess.CalledProcessError) as context:
19-
subprocess.check_output(f"python3 {PROJECT_ROOT}/main.py", shell=True, stderr=subprocess.STDOUT).strip()
20-
self.assertIn(context.exception.message, "main.py: error: the following arguments are required: -s/--string")
20+
subprocess.check_output(f"python3 {PROJECT_ROOT}/src/main.py", shell=True, stderr=subprocess.STDOUT).strip()
21+
self.assertIn("main.py: error: the following arguments are required: -s/--string", context.exception.output.decode('utf-8'))
2122

2223

2324
class TestHappyPath(unittest.TestCase):
2425
def test_successfully_creates_identicon(self):
2526
identicon = Identicon("931D387731bBbC988B31220")
2627
identicon.draw_image(filename="output")
27-
image = Image.open(f"{PROJECT_ROOT}/output.png", mode="r")
28-
self.assertIsInstance(image, Image, "Image created is not of type PIL.Image")
28+
generated_image = Image.open(f"{PROJECT_ROOT}/output.png", mode="r")
29+
self.assertIsInstance(generated_image, PngImagePlugin.PngImageFile)
30+
remove(f"{PROJECT_ROOT}/output.png")
31+
32+
def test_successfully_creates_same_identicon_for_same_input_strings(self):
33+
# Make 1st identicon
34+
identicon_john_1 = Identicon("john")
35+
identicon_john_1.draw_image(filename="john1")
36+
# Make 2nd identicon
37+
identicon_john_2 = Identicon("john")
38+
identicon_john_2.draw_image(filename="john2")
39+
40+
# Assertions
41+
generated_john_1 = Image.open(f"{PROJECT_ROOT}/john1.png", mode="r")
42+
self.assertIsInstance(generated_john_1, PngImagePlugin.PngImageFile)
43+
44+
generated_john_2 = Image.open(f"{PROJECT_ROOT}/john2.png", mode="r")
45+
self.assertIsInstance(generated_john_2, PngImagePlugin.PngImageFile)
46+
47+
self.assertEqual(generated_john_1, generated_john_2)
48+
49+
# Cleanup
50+
remove(f"{PROJECT_ROOT}/john1.png")
51+
remove(f"{PROJECT_ROOT}/john2.png")
52+
53+
def test_does_not_create_same_identicon_for_different_input_strings(self):
54+
# Make 1st identicon
55+
identicon_john = Identicon("john")
56+
identicon_john.draw_image(filename="john")
57+
# Make 2nd identicon
58+
identicon_john_2 = Identicon("jane")
59+
identicon_john_2.draw_image(filename="jane")
60+
61+
# Assertions
62+
generated_john = Image.open(f"{PROJECT_ROOT}/john.png", mode="r")
63+
self.assertIsInstance(generated_john, PngImagePlugin.PngImageFile)
64+
65+
generated_jane = Image.open(f"{PROJECT_ROOT}/jane.png", mode="r")
66+
self.assertIsInstance(generated_jane, PngImagePlugin.PngImageFile)
67+
68+
self.assertNotEqual(generated_john, generated_jane)
69+
70+
# Cleanup
71+
remove(f"{PROJECT_ROOT}/john.png")
72+
remove(f"{PROJECT_ROOT}/jane.png")
73+
2974

30-
# hash_str =convert_string_to_sha_hash("931D387731bBbC988B31220")
31-
# hash_str = convert_string_to_sha_hash("[email protected]")
32-
# grid = build_grid(hash_str)
33-
# draw_image(grid, hash_str)
3475

3576
if __name__ == '__maipython -m unittest__':
3677
unittest.main()

0 commit comments

Comments
 (0)