-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from benhovinga/fix_rename_symbols_coherently
Restructured the project
- Loading branch information
Showing
19 changed files
with
579 additions
and
398 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 |
---|---|---|
|
@@ -3,5 +3,4 @@ htmlcov | |
.coverage | ||
.DS_Store | ||
.vscode | ||
_old_stuff | ||
.pytest_cache |
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,34 @@ | ||
from __future__ import annotations | ||
from typing import NamedTuple, Iterable | ||
|
||
from .file_header import FileHeader | ||
from .subfile_designator import SubfileDesignator | ||
from .subfile import Subfile | ||
|
||
from .utils import trim_before | ||
|
||
|
||
class BarcodeFile(NamedTuple): | ||
header: FileHeader | ||
subfiles: Iterable[Subfile] | ||
|
||
@classmethod | ||
def parse(cls, file:str) -> BarcodeFile: | ||
file = trim_before(FileHeader.COMPLIANCE_INDICATOR, file) | ||
header = FileHeader.parse(file) | ||
if header.number_of_entries < 1: | ||
raise ValueError("number of entries cannot be less than 1") | ||
|
||
subfiles = list() | ||
for i in range(header.number_of_entries): | ||
designator = SubfileDesignator.parse( | ||
file=file, | ||
aamva_version=header.aamva_version, | ||
designator_index=i) | ||
subfile = Subfile.parse(file, designator) | ||
subfiles.append(subfile) | ||
|
||
return cls( | ||
header=header, | ||
subfiles=tuple(subfiles) | ||
) |
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,102 @@ | ||
from __future__ import annotations | ||
from typing import NamedTuple, Optional | ||
|
||
from ..errors import InvalidHeaderError | ||
|
||
|
||
class FileHeader(NamedTuple): | ||
""" | ||
Represents the Header of a file that would be stored in a barcode | ||
""" | ||
issuer_id: int | ||
aamva_version: int | ||
number_of_entries: int | ||
jurisdiction_version: Optional[int] = 0 | ||
|
||
# Static header elements | ||
COMPLIANCE_INDICATOR = "@" | ||
DATA_ELEMENT_SEPARATOR = "\n" | ||
RECORD_SEPARATOR = "\x1e" | ||
SEGMENT_TERMINATOR = "\r" | ||
FILE_TYPE = "ANSI " | ||
|
||
@staticmethod | ||
def header_length(aamva_version: int): | ||
""" | ||
Returns the length of the header based on the AAMVA version. In version | ||
2 of the AAMVA Standard the header length increased from 19 bytes to 21 | ||
bytes. This is to accomidate a new 2 byte field called "jurisdiction | ||
version number" in the header. | ||
Args: | ||
version (int): The AAMVA version number. | ||
Returns: | ||
int: The length of the header (19 or 21) | ||
""" | ||
return 19 if aamva_version < 2 else 21 | ||
|
||
@classmethod | ||
def parse(cls, file: str) -> FileHeader: | ||
""" | ||
Parses the file header and returns a structured Header object. | ||
Args: | ||
file (str): Output from a barcode scanner. | ||
Returns: | ||
FileHeader: The file header object. | ||
Raises: | ||
IndexError: If the header length is too short. | ||
InvalidHeaderError: If a header element contains invalid data. | ||
""" | ||
MIN_LENGTH = 17 | ||
|
||
# Validation | ||
if len(file) < MIN_LENGTH: | ||
raise IndexError("Header length is too short.") | ||
elif file[0] != cls.COMPLIANCE_INDICATOR: | ||
raise InvalidHeaderError("COMPLIANCE_INDICATOR") | ||
elif file[1] != cls.DATA_ELEMENT_SEPARATOR: | ||
raise InvalidHeaderError("DATA_ELEMENT_SEPARATOR") | ||
elif file[2] != cls.RECORD_SEPARATOR: | ||
raise InvalidHeaderError("RECORD_SEPARATOR") | ||
elif file[3] != cls.SEGMENT_TERMINATOR: | ||
raise InvalidHeaderError("SEGMENT_TERMINATOR") | ||
elif file[4:9] != cls.FILE_TYPE: | ||
raise InvalidHeaderError("FILE_TYPE") | ||
|
||
aamva_version = int(file[15:17]) | ||
if len(file) < cls.header_length(aamva_version): | ||
raise IndexError("Header length is too short.") | ||
|
||
if aamva_version < 2: | ||
return cls( | ||
issuer_id=int(file[9:15]), | ||
aamva_version=aamva_version, | ||
number_of_entries=int(file[17:19]) | ||
) | ||
return cls( | ||
issuer_id=int(file[9:15]), | ||
aamva_version=aamva_version, | ||
number_of_entries=int(file[19:21]), | ||
jurisdiction_version=int(file[17:19]) | ||
) | ||
|
||
def unparse(self) -> str: | ||
"""Converts the structured Header object into a file header string. | ||
Returns: | ||
str: file header | ||
""" | ||
jurisdiction = str(self.jurisdiction_version).rjust(2, '0') if self.aamva_version > 1 else "" | ||
return self.COMPLIANCE_INDICATOR + \ | ||
self.DATA_ELEMENT_SEPARATOR + \ | ||
self.RECORD_SEPARATOR + \ | ||
self.SEGMENT_TERMINATOR + \ | ||
self.FILE_TYPE.ljust(5) + \ | ||
str(self.issuer_id).rjust(6, '0') + \ | ||
str(self.aamva_version).rjust(2, '0') + \ | ||
jurisdiction + \ | ||
str(self.number_of_entries).rjust(2, '0') |
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,32 @@ | ||
from __future__ import annotations | ||
from typing import NamedTuple | ||
|
||
from .subfile_designator import SubfileDesignator | ||
from .file_header import FileHeader | ||
|
||
class Subfile(NamedTuple): | ||
subfile_type: str | ||
elements: dict | ||
|
||
@classmethod | ||
def parse(cls, file: str, designator: SubfileDesignator) -> Subfile: | ||
subfile_type = designator.subfile_type | ||
offset = designator.offset | ||
length = designator.length | ||
end_offset = offset + length - 1 | ||
|
||
if file[offset:offset + 2] != subfile_type: | ||
raise ValueError("Subfile is missing subfile type.") | ||
elif file[end_offset] != FileHeader.SEGMENT_TERMINATOR: | ||
raise ValueError("Subfile is missing segment terminator.") | ||
|
||
items = filter(None, file[offset + 2: end_offset].split(FileHeader.DATA_ELEMENT_SEPARATOR)) | ||
|
||
elements = dict() | ||
for item in items: | ||
elements[item[:3]] = item[3:] | ||
|
||
return cls( | ||
subfile_type=subfile_type, | ||
elements=elements | ||
) |
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,29 @@ | ||
from __future__ import annotations | ||
from typing import NamedTuple | ||
|
||
from .file_header import FileHeader | ||
|
||
|
||
class SubfileDesignator(NamedTuple): | ||
subfile_type: str | ||
offset: int | ||
length: int | ||
|
||
@classmethod | ||
def parse(cls, file: str, aamva_version: int, designator_index: int) -> SubfileDesignator: | ||
DESIGNATOR_LENGTH = 10 | ||
cursor = designator_index * DESIGNATOR_LENGTH + FileHeader.header_length(aamva_version) | ||
|
||
if len(file) < cursor + DESIGNATOR_LENGTH: | ||
raise IndexError("Subfile designator too short.") | ||
|
||
return cls( | ||
subfile_type=str(file[cursor:cursor + 2]), | ||
offset=int(file[cursor + 2:cursor + 6]), | ||
length=int(file[cursor + 6:cursor + 10]) | ||
) | ||
|
||
def unparse(self): | ||
return self.subfile_type + \ | ||
str(self.offset).rjust(4, "0") + \ | ||
str(self.length).rjust(4, "0") |
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,24 @@ | ||
def trim_before(char: str, _str: str) -> str: | ||
""" | ||
Removes everything before the first instance of a character in a given string. | ||
Args: | ||
char (str): A character to search for in the string. | ||
_str (str): The string to modify. | ||
Returns: | ||
str: The modified string. | ||
Raises: | ||
TypeError: When char doesn't have a length of 1. | ||
ValueError: If char is not found in the string. | ||
""" | ||
if type(char) != str or len(char) != 1: | ||
raise TypeError(f"char must have a length of 1") | ||
if _str[0] != char: | ||
try: | ||
index = _str.index(char) | ||
except ValueError: | ||
raise ValueError(f"Character \"{char}\" not found in string") | ||
_str = _str[index:] | ||
return _str |
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,5 @@ | ||
class InvalidHeaderError(Exception): | ||
"""Raised when a header element contains invalid data.""" | ||
def __init__(self, header_element: str, *args: object) -> None: | ||
message = f"Header element '{header_element}' contains invalid data." | ||
super().__init__(message, *args) |
File renamed without changes.
Oops, something went wrong.