from . import deprecation
PRODUCT_EAN_LENGTH = 7
def bcd_digits(bcd_bytes):
"""Split a byte sequence into four-bit BCD digits
Used internally by `EAN` to decode BCD.
For example 0x12345 is turned into 1, 2, 3, 4, 5.
`bcd_bytes` must be an iterable of eight bit objects supporting `&` and
`>>`.
Note:
The byteorder is currently assumed to be 'little'.
"""
# byteorder is assumed to be 'little'
for char in bcd_bytes:
# Python 2 doesn't have bytes, only string
if isinstance(char, str):
char = ord(char)
yield char & 0xF
yield char >> 4
def int_from_digits(digits, base=10):
"""Joins a sequence of decimal digits into a decimal number
Used internally by `EAN`.
For example (1, 2, 3, 4, 5) is turned into 54321.
Iterating through `digits` is assumed to only yield integers between 0 and
9, inclusive.
"""
decimal = 0
for pos, digit in enumerate(digits):
decimal += digit * base ** pos
return decimal
class IllegalEAN(ValueError):
"""Could not parse EAN"""
pass
[docs]class EAN:
r"""Helper object for dealing with European Article Numbers
Depending on the format the ean is in, `EAN` objects are created in
different ways;
For strings::
EAN('73-30130-01234-5')
For integers::
EAN(7330130012345)
For iterables of integers::
EAN([7, 3, 3, 0, 1, 3, 0, 0, 1, 2, 3, 4, 5])
For BCD-coded bytes or bytearrays (str in python 2)::
EAN.from_bcd(b'\x45\x23\x01\x30\x01\x33\x07')
For "hi-lo" format, i.e. two 32-bit integers containing half the ean each,
both BCD-coded::
EAN.from_hilo([eanHi, eanLo])
The various representations can then be produced from the resulting object::
>>> str(ean)
'73-30130-01234-5'
>>> int(ean)
7330130012345
>>> tuple(ean) # or list(), or any other sequence type
(7, 3, 3, 0, 1, 3, 0, 0, 1, 2, 3, 4, 5)
>>> ean.bcd()
b'E#\x010\x013\x07'
>>> ean.hilo()
(805380933, 471809)
Sometimes it is easier to only use the last six digits of the ean, the
product code and check digit. This is supported when working with string
representations; the constructor supports six-digit (seven-character) input::
EAN('01234-5')
In that cases, the country and manufacturer code is assumed to be that of
Kvaser AB (73-30130).
A string containing only the product code and check digit can also be retrieved::
ean.product()
Instances can also be indexed which yields specific digits as integers::
>>> ean[7]
0
>>> ean[7:]
(0, 1, 2, 3, 4, 5)
Note:
The byteorder is currently always assumed to be 'little'.
"""
fmt = "##-#####-#####-#"
num_digits = len([s for s in fmt if s == '#'])
[docs] @classmethod
def from_bcd(cls, bcd_bytes):
"""Create an EAN object from a binary coded bytes-like object
The EAN is automatically shortened to the correct length.
"""
# The digits are in reverse order, and there is an extra zero
digits = tuple(bcd_digits(bcd_bytes))[: cls.num_digits]
digits = reversed(digits)
return cls(digits)
[docs] @classmethod
@deprecation.deprecated.favour("the EAN constructor directly")
def from_string(cls, ean_string):
"""Create an EAN object from a specially formatted string
.. deprecated:: 1.6
Use the constructor, `EAN(ean_string)`, instead.
"""
return cls(ean_string)
[docs] @classmethod
def from_hilo(cls, hilo):
"""Create an EAN object from a pair of 32-bit integers, (eanHi, eanLo)"""
high, low = hilo
# we get three extra digits in 'high'
high = tuple(bcd_digits(high.to_bytes(4, byteorder='little')))[:-3]
low = tuple(bcd_digits(low.to_bytes(4, byteorder='little')))
return cls(high[::-1] + low[::-1]) # and the digits are reversed
@classmethod
def _parse_int(cls, ean_int):
digits_string = str(ean_int).rjust(cls.num_digits, '0')
internal = tuple(int(d) for d in digits_string)
if len(internal) != cls.num_digits:
raise IllegalEAN("Too large EAN integer: " + str(ean_int))
else:
return internal
@classmethod
def _parse_str(cls, ean_string):
if len(ean_string) == PRODUCT_EAN_LENGTH:
ean_string = "73-30130-" + ean_string
if len(ean_string) != len(cls.fmt):
raise IllegalEAN("Wrong length for EAN string: " + repr(ean_string))
if not all(s.isdigit() if (f == '#') else (f == s) for f, s in zip(cls.fmt, ean_string)):
raise IllegalEAN("Unreconized format for EAN string: " + repr(ean_string))
internal = tuple(int(s) for f, s in zip(cls.fmt, ean_string) if f == '#')
return internal
def __init__(self, source):
if isinstance(source, str):
self._internal = self._parse_str(source)
elif isinstance(source, int):
self._internal = self._parse_int(source)
else:
# Assumed to be a iterable
internal = tuple(source)
if len(internal) != self.num_digits:
raise IllegalEAN(f"Wrong length of EAN sequence ({len(internal)})")
elif not all(isinstance(d, int) for d in internal):
raise IllegalEAN("EAN sequence must contain only ints")
else:
self._internal = internal
# required in Python 2
def __ne__(self, other):
return not self == other
def __eq__(self, other):
if isinstance(other, EAN):
return self._internal == other._internal
elif isinstance(other, str):
return str(self) == other
else:
return NotImplemented
def __getitem__(self, index):
return self._internal[index]
def __int__(self):
return int_from_digits(reversed(self._internal))
def __iter__(self):
return iter(self._internal)
def __str__(self):
num_only = map(str, self._internal)
out = ''.join(next(num_only) if s == '#' else s for s in self.fmt)
if __debug__:
# check that all digits where printed
rest = tuple(num_only)
assert len(rest) == 0, rest
return out
def __repr__(self):
return f"<{self.__class__.__name__}: {str(self)}>"
def __hash__(self):
return hash(str(self))
[docs] def bcd(self):
"""Return a binary-coded bytes object with this EAN"""
digits_string = ''.join(str(d) for d in self._internal)
# fromhex requires an even number of digits
bcd = bytes.fromhex('0' + digits_string)
bcd = bytes(reversed(bcd))
return bcd
[docs] def hilo(self):
"""Return a pair of 32-bit integers, (eanHi, eanLo), with this EAN"""
high = self._internal[:-8]
low = self._internal[-8:]
high = int_from_digits(reversed(high), base=16)
low = int_from_digits(reversed(low), base=16)
return (high, low)
[docs] def product(self):
"""Return only the product code and check digit of the string representation"""
return str(self)[-PRODUCT_EAN_LENGTH:]