Skip to content

Commit

Permalink
Merge pull request #9 from benhovinga/fix_rename_symbols_coherently
Browse files Browse the repository at this point in the history
Restructured the project
  • Loading branch information
benhovinga authored May 11, 2024
2 parents 47e0575 + 502bf33 commit 5602a02
Show file tree
Hide file tree
Showing 19 changed files with 579 additions and 398 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ htmlcov
.coverage
.DS_Store
.vscode
_old_stuff
.pytest_cache
Empty file added aamva/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions aamva/barcode_file/__init__.py
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)
)
102 changes: 102 additions & 0 deletions aamva/barcode_file/file_header.py
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')
32 changes: 32 additions & 0 deletions aamva/barcode_file/subfile.py
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
)
29 changes: 29 additions & 0 deletions aamva/barcode_file/subfile_designator.py
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")
24 changes: 24 additions & 0 deletions aamva/barcode_file/utils.py
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
5 changes: 5 additions & 0 deletions aamva/errors.py
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.
Loading

0 comments on commit 5602a02

Please sign in to comment.