Implement OpenSSH style identicon calculation
This commit is contained in:
commit
0dd22bd7bc
1
identicon/__init__.py
Normal file
1
identicon/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .identicon import Identicon
|
89
identicon/identicon.py
Normal file
89
identicon/identicon.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Coordinate:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
return self.__class__(
|
||||||
|
self.x+other.x,
|
||||||
|
self.y+other.y
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Identicon:
|
||||||
|
WIDTH = 17
|
||||||
|
HEIGHT = 9
|
||||||
|
SYMBOLS = ' .o+=*BOX@%&#/^'
|
||||||
|
START_SYMBOL = 'S'
|
||||||
|
END_SYMBOL = 'E'
|
||||||
|
BISHOP_START = Coordinate(8, 4)
|
||||||
|
|
||||||
|
MOVES = {
|
||||||
|
0: Coordinate(-1, -1), # 00 ↖
|
||||||
|
1: Coordinate( 1, -1), # 01 ↗
|
||||||
|
2: Coordinate(-1, 1), # 10 ↙
|
||||||
|
3: Coordinate( 1, 1), # 11 ↘
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self._data = data
|
||||||
|
self._grid = [[0] * self.WIDTH for _ in range(self.HEIGHT)]
|
||||||
|
self._bishop_position = deepcopy(self.BISHOP_START)
|
||||||
|
self._end_position = None
|
||||||
|
|
||||||
|
def calculate(self):
|
||||||
|
for command in self._get_commands():
|
||||||
|
self._execute(command)
|
||||||
|
self._end_position = self._bishop_position
|
||||||
|
|
||||||
|
def _get_commands(self):
|
||||||
|
commands = []
|
||||||
|
for octet in self._data:
|
||||||
|
commands.extend([
|
||||||
|
octet & 0b11, # 00 00 00 [00]
|
||||||
|
(octet >> 2) & 0b11, # 00 00 [00] 00
|
||||||
|
(octet >> 4) & 0b11, # 00 [00] 00 00
|
||||||
|
(octet >> 6) & 0b11, # [00] 00 00 00
|
||||||
|
])
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _execute(self, command):
|
||||||
|
move = self.MOVES[command]
|
||||||
|
self._bishop_position = self._ensure_within_grid_borders(self._bishop_position + move)
|
||||||
|
self._increment_current_position()
|
||||||
|
|
||||||
|
def _ensure_within_grid_borders(self, coordinate: Coordinate):
|
||||||
|
coordinate.x = max(0, min(self.WIDTH-1, coordinate.x))
|
||||||
|
coordinate.y = max(0, min(self.HEIGHT-1, coordinate.y))
|
||||||
|
return coordinate
|
||||||
|
|
||||||
|
def _increment_current_position(self):
|
||||||
|
self._grid[self._bishop_position.y][self._bishop_position.x] += 1
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
icon = self._header+'\n'
|
||||||
|
for y, row in enumerate(self._grid):
|
||||||
|
icon += '|'
|
||||||
|
for x, cell in enumerate(row):
|
||||||
|
icon += self._determine_symbol(Coordinate(x, y), cell)
|
||||||
|
icon += '|\n'
|
||||||
|
icon += self._header
|
||||||
|
|
||||||
|
return icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _header(self):
|
||||||
|
return f'+{self.WIDTH*"-"}+'
|
||||||
|
|
||||||
|
def _determine_symbol(self, coordinate, cell):
|
||||||
|
symbol = self.SYMBOLS[min(cell, len(self.SYMBOLS)-1)]
|
||||||
|
if coordinate == self.BISHOP_START:
|
||||||
|
symbol = self.START_SYMBOL
|
||||||
|
elif coordinate == self._end_position:
|
||||||
|
symbol = self.END_SYMBOL
|
||||||
|
|
||||||
|
return symbol
|
89
identicon/test_identicon.py
Normal file
89
identicon/test_identicon.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
from base64 import b64decode
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .identicon import Identicon
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReferenceIcon:
|
||||||
|
fingerprint: bytes
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_ICONS = [
|
||||||
|
ReferenceIcon(
|
||||||
|
bytearray.fromhex(''.join('b7:a3:e2:ce:09:06:ad:39:c8:1d:ad:b5:95:48:8f:99'.split(':'))),
|
||||||
|
dedent('''\
|
||||||
|
+-----------------+
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| . |
|
||||||
|
| .o * . |
|
||||||
|
| ...E +S . |
|
||||||
|
|...++ o . . |
|
||||||
|
|..+oo. o |
|
||||||
|
| o o.. . . |
|
||||||
|
| o=.. |
|
||||||
|
+-----------------+'''
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ReferenceIcon(
|
||||||
|
b64decode('sX58LC41tVlsctG1+H5PrkbMDfG374yghEg96KlnFZA=='),
|
||||||
|
dedent('''\
|
||||||
|
+-----------------+
|
||||||
|
| . .o|
|
||||||
|
| E + o|
|
||||||
|
| .. o = |
|
||||||
|
| o.o o B o|
|
||||||
|
| o S. . X +o|
|
||||||
|
| o +.+o.o =..|
|
||||||
|
| +.o.=.+. .+|
|
||||||
|
| .o .+ + ..=+|
|
||||||
|
| .o .o .oo=|
|
||||||
|
+-----------------+'''
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ReferenceIcon(
|
||||||
|
bytearray.fromhex(''.join('9b:cf:42:fd:25:ff:ce:83:e9:e0:f1:d4:10:c3:ae:a8'.split(':'))),
|
||||||
|
dedent('''\
|
||||||
|
+-----------------+
|
||||||
|
| |
|
||||||
|
| . |
|
||||||
|
| + |
|
||||||
|
| . o |
|
||||||
|
| S. o |
|
||||||
|
| .oo o + |
|
||||||
|
| .o. = =o. |
|
||||||
|
| oo. *o.o |
|
||||||
|
| E .o..o o=|
|
||||||
|
+-----------------+'''
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ReferenceIcon(
|
||||||
|
b64decode('5/WozN6loc0G8DxxrJhV8+aG/6Dvz1/gTBVipWU9nb0='),
|
||||||
|
dedent('''\
|
||||||
|
+-----------------+
|
||||||
|
| o.==|
|
||||||
|
| + =o=|
|
||||||
|
| o + +|
|
||||||
|
| . o o oE |
|
||||||
|
| SB.+.+o |
|
||||||
|
| oo*..*o. |
|
||||||
|
| .ooo* .|
|
||||||
|
| o *.=.o.|
|
||||||
|
| .*.*ooo*|
|
||||||
|
+-----------------+'''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reference", REFERENCE_ICONS)
|
||||||
|
def test_identicon(reference):
|
||||||
|
icon = Identicon(reference.fingerprint)
|
||||||
|
icon.calculate()
|
||||||
|
|
||||||
|
assert str(icon) == reference.icon
|
Loading…
Reference in New Issue
Block a user