Skip to content

Commit

Permalink
Merge pull request #21 from nedap/lgtin
Browse files Browse the repository at this point in the history
LGTIN class and restructure
  • Loading branch information
SanderMeinderts authored Mar 28, 2022
2 parents 08caa06 + 0d58802 commit f5a454f
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ An example highlighting the different options for the `SGTIN` scheme can be foun
| GSRNP | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| IMOVN | | | |
| ITIP | :heavy_check_mark: | | :heavy_check_mark: |
| LGTIN | :heavy_check_mark: | :heavy_check_mark: | |
| PGLN | :heavy_check_mark: | :heavy_check_mark: | |
| SGCN | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| SGLN | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
Expand Down
1 change: 1 addition & 0 deletions epcpy/epc_schemes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .gsrnp import GSRNP
from .imovn import IMOVN
from .itip import ITIP
from .lgtin import LGTIN
from .pgln import PGLN
from .sgcn import SGCN
from .sgln import SGLN
Expand Down
149 changes: 149 additions & 0 deletions epcpy/epc_schemes/lgtin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from __future__ import annotations

import re
from enum import IntEnum

from epcpy.epc_schemes.base_scheme import EPCScheme, GS1Keyed
from epcpy.utils.common import (
ConvertException,
calculate_checksum,
replace_uri_escapes,
revert_uri_escapes,
verify_gs3a3_component,
)
from epcpy.utils.regex import LGTIN_CLASS, LGTIN_GS1_ELEMENT_STRING

LGTIN_CLASS_REGEX = re.compile(LGTIN_CLASS)


class GTIN_TYPE(IntEnum):
GTIN8 = (8,)
GTIN12 = (12,)
GTIN13 = (13,)
GTIN14 = 14


class LGTIN(EPCScheme, GS1Keyed):
"""LGTIN EPC scheme implementation.
LGTIN pure identities are of the form:
urn:epc:class:lgtin:<CompanyPrefix>.<ItemRefAndIndicator>.<Lot>
Example:
urn:epc:class:lgtin:061414.0123456.xyz3311cba
This class can be created using EPC pure identities via its constructor, or using:
- LGTIN.from_gs1_element_string
Attributes:
gs1_key (str): GS1 key
gs1_element_string (str): GS1 element string
"""

gs1_element_string_regex = re.compile(LGTIN_GS1_ELEMENT_STRING)

def __init__(self, epc_class) -> None:
super().__init__()

if not LGTIN_CLASS_REGEX.fullmatch(epc_class):
raise ConvertException(message=f"Invalid LGTIN CLASS {epc_class}")

self._company_pref, self._item_ref, *lot = ":".join(
epc_class.split(":")[4:]
).split(".")

self._lot = ".".join(lot)
verify_gs3a3_component(self._lot)
self._lot = replace_uri_escapes(self._lot)

if not (6 <= len(self._company_pref) <= 12):
raise ConvertException(
message=f"Invalid company prefix length {len(self._company_pref)}"
)

if len(f"{self._company_pref}{self._item_ref}") != 13:
raise ConvertException(
message=f"Complete component length of invalid length (!=13)"
)

if not (1 <= len(self._lot) <= 20):
raise ConvertException(message="Invalid lot length")

self.epc_uri = epc_class

check_digit = calculate_checksum(
f"{self._item_ref[0]}{self._company_pref}{self._item_ref[1:]}"
)

self._gtin = f"{self._item_ref[0]}{self._company_pref}{self._item_ref[1:]}{check_digit}".zfill(
14
)

def gs1_key(self, gtin_type=GTIN_TYPE.GTIN14) -> str:
"""GS1 key belonging to this LGTIN instance
Args:
gtin_type (GTIN_TYPE, optional): What GTIN length to return.
Defaults to GTIN_TYPE.GTIN14.
Returns:
str: GS1 key
"""
return self.gtin(gtin_type=gtin_type)

def gtin(self, gtin_type=GTIN_TYPE.GTIN14) -> str:
"""GTIN belonging to this LGTIN instance
Args:
gtin_type (GTIN_TYPE, optional): What GTIN length to return.
Defaults to GTIN_TYPE.GTIN14.
Raises:
ConvertException: GTIN does not match given type
Returns:
str: GTIN
"""
if gtin_type != GTIN_TYPE.GTIN14:
if not self._gtin.startswith((14 - gtin_type) * "0"):
raise ConvertException(message=f"Invalid GTIN{gtin_type}")

return self._gtin[14 - gtin_type : 14]

def gs1_element_string(self) -> str:
"""Returns the GS1 element string
Returns:
str: GS1 element string
"""
gtin = self._gtin

return f"(01){gtin}(10){self._lot}"

@classmethod
def from_gs1_element_string(
cls, gs1_element_string: str, company_prefix_length: int
) -> LGTIN:
"""Create a LGTIN instance from a GS1 element string and company prefix
Args:
gs1_element_string (str): GS1 element string
company_prefix_length (int): Company prefix length
Raises:
ConvertException: LGTIN GS1 element string invalid
Returns:
LGTIN: LGTIN scheme
"""
if not LGTIN.gs1_element_string_regex.fullmatch(gs1_element_string):
raise ConvertException(
message=f"Invalid LGTIN GS1 element string {gs1_element_string}"
)

_, digits, chars = re.split(f"\(.{{2}}\)", gs1_element_string)
chars = revert_uri_escapes(chars)

return cls(
f"urn:epc:class:lgtin:{digits[1:company_prefix_length+1]}.{digits[0]}{digits[1+company_prefix_length:-1]}.{chars}"
)
2 changes: 2 additions & 0 deletions epcpy/utils/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
GSRNP,
IMOVN,
ITIP,
LGTIN,
PGLN,
SGCN,
SGLN,
Expand Down Expand Up @@ -51,6 +52,7 @@
GSRNP,
IMOVN,
ITIP,
LGTIN,
PGLN,
SGCN,
SGLN,
Expand Down
8 changes: 6 additions & 2 deletions epcpy/utils/regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@
IMOVN_URI_BODY = "[0-9]{7}"
IMOVN_URI = f"urn:epc:id:imovn:{IMOVN_URI_BODY}"

LGTIN_CLASS = f"urn:epc:class:lgtin:{PADDED_NUMERIC_COMPONENT}\.{PADDED_NUMERIC_COMPONENT}\.{GS3A3_COMPONENT}"

EPC_URI = (
f"({SGTIN_URI}|{SSCC_URI}|{SGLN_URI}|{GRAI_URI}|{GIAI_URI}|{GSRN_URI}|{GSRNP_URI}|{GDTI_URI}"
f"|{CPI_URI}|{SGCN_URI}|{GINC_URI}|{GSIN_URI}|{ITIP_URI}|{UPUI_URI}|{PGLN_URI}|{GID_URI}"
f"|{USDOD_URI}|{ADI_URI}|{BIC_URI}|{IMOVN_URI})"
f"|{USDOD_URI}|{ADI_URI}|{BIC_URI}|{IMOVN_URI}|{LGTIN_CLASS})"
)

# EPC IDPAT URIs
Expand Down Expand Up @@ -155,11 +157,13 @@
ITIP_GS1_ELEMENT_STRING = f"\(8006\){DIGIT}{{18}}\(21\){GS1_ELEM_CHARS}{{1,20}}"
UPUI_GS1_ELEMENT_STRING = f"\(01\){DIGIT}{{14}}\(235\){GS1_ELEM_CHARS}{{1,28}}"
PGLN_GS1_ELEMENT_STRING = f"\(417\){DIGIT}{{13}}"
LGTIN_GS1_ELEMENT_STRING = f"\(01\){DIGIT}{{14}}\(10\){GS1_ELEM_CHARS}{{1,20}}"

GS1_ELEMENT_STRING = (
f"({SGTIN_GS1_ELEMENT_STRING}|{SSCC_GS1_ELEMENT_STRING}|{SGLN_GS1_ELEMENT_STRING}"
f"|{GRAI_GS1_ELEMENT_STRING}|{GIAI_GS1_ELEMENT_STRING}|{GSRN_GS1_ELEMENT_STRING}"
f"|{GSRNP_GS1_ELEMENT_STRING}|{GDTI_GS1_ELEMENT_STRING}|{CPI_GS1_ELEMENT_STRING}"
f"|{SGCN_GS1_ELEMENT_STRING}|{GINC_GS1_ELEMENT_STRING}|{GSIN_GS1_ELEMENT_STRING}"
f"|{ITIP_GS1_ELEMENT_STRING}|{UPUI_GS1_ELEMENT_STRING}|{PGLN_GS1_ELEMENT_STRING})"
f"|{ITIP_GS1_ELEMENT_STRING}|{UPUI_GS1_ELEMENT_STRING}|{PGLN_GS1_ELEMENT_STRING}"
f"|{LGTIN_GS1_ELEMENT_STRING})"
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "epcpy"
version = "0.1.5"
version = "0.1.6"
description = "A Python module for creation, validation, and transformation of EPC representations as defined in GS1's EPC Tag Data Standard."
license = "MIT"
authors = ["Nedap Retail <[email protected]>"]
Expand Down
120 changes: 120 additions & 0 deletions tests/epc_schemes/test_lgtin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import unittest

from epcpy.epc_schemes.lgtin import GTIN_TYPE, LGTIN
from tests.epc_schemes.test_base_scheme import TestEPCSchemeInitMeta, TestGS1ElementMeta


class TestLGTINInit(
unittest.TestCase,
metaclass=TestEPCSchemeInitMeta,
scheme=LGTIN,
valid_data=[
{
"name": "test_valid_lgtin_1",
"uri": "urn:epc:class:lgtin:061414.0123456.xyz3311cba",
},
{
"name": "test_valid_lgtin_2",
"uri": "urn:epc:class:lgtin:061414012345.0.0",
},
{
"name": "test_valid_lgtin_3",
"uri": "urn:epc:class:lgtin:061414012345.0.01234567890123456789",
},
],
invalid_data=[
{
"name": "test_invalid_lgtin_identifier",
"uri": "urn:epc:class:lgtn:06141.10123456.xyz3311cba",
},
{
"name": "test_invalid_lgtin_company_prefix_1",
"uri": "urn:epc:class:lgtin:06141.10123456.01ABc%2FD",
},
{
"name": "test_invalid_lgtin_company_prefix_2",
"uri": "urn:epc:class:lgtin:0614140123456.0.1",
},
{
"name": "test_invalid_lgtin_invalid_component",
"uri": "urn:epc:class:lgtin:06141.0.01ABc%2FD",
},
{
"name": "test_invalid_lgtin_serial_too_long_1",
"uri": "urn:epc:class:lgtin:012345.0123456.012345678901234567890",
},
],
):
pass


class TestLGTINGS1Key(
unittest.TestCase,
metaclass=TestGS1ElementMeta,
scheme=LGTIN,
valid_data=[
{
"name": "test_valid_lgtin_gs1_key_1",
"uri": "urn:epc:class:lgtin:50712192365.88..%25:.13%26",
"gs1_key": "85071219236581",
"gs1_element_string": "(01)85071219236581(10).%:.13&",
"company_prefix_length": 11,
},
{
"name": "test_valid_lgtin_gs1_key_2",
"uri": "urn:epc:class:lgtin:00000000000.00.0",
"gs1_key": "00000000000000",
"gs1_element_string": "(01)00000000000000(10)0",
"company_prefix_length": 11,
},
{
"name": "test_valid_lgtin_gs1_key_3",
"uri": "urn:epc:class:lgtin:5019265.123588..%25:.13%26",
"gs1_key": "15019265235883",
"gs1_element_string": "(01)15019265235883(10).%:.13&",
"company_prefix_length": 7,
},
{
"name": "test_valid_lgtin_gs1_key_4",
"uri": "urn:epc:class:lgtin:00000950.01093.Serial",
"kwargs": {"gtin_type": GTIN_TYPE.GTIN13},
"gs1_key": "0000095010939",
"gs1_element_string": "(01)00000095010939(10)Serial",
"company_prefix_length": 8,
},
{
"name": "test_valid_lgtin_gs1_key_5",
"uri": "urn:epc:class:lgtin:00000950.01093.Serial",
"kwargs": {"gtin_type": GTIN_TYPE.GTIN12},
"gs1_key": "000095010939",
"gs1_element_string": "(01)00000095010939(10)Serial",
"company_prefix_length": 8,
},
{
"name": "test_valid_lgtin_gs1_key_6",
"uri": "urn:epc:class:lgtin:00000950.01093.Serial",
"kwargs": {"gtin_type": GTIN_TYPE.GTIN8},
"gs1_key": "95010939",
"gs1_element_string": "(01)00000095010939(10)Serial",
"company_prefix_length": 8,
},
],
invalid_data=[
{
"name": "test_invalid_lgtin_gs1_key_too_little_zeros_gtin13",
"uri": "urn:epc:class:lgtin:5019265.123588..%25:.13%26",
"kwargs": {"gtin_type": GTIN_TYPE.GTIN13},
},
{
"name": "test_invalid_lgtin_gs1_key_too_little_zeros_gtin12",
"uri": "urn:epc:class:lgtin:1519265.023588..%25:.13%26",
"kwargs": {"gtin_type": GTIN_TYPE.GTIN12},
},
{
"name": "test_invalid_lgtin_gs1_key_too_little_zeros_gtin8",
"uri": "urn:epc:class:lgtin:0000565.023588..%25:.13%26",
"kwargs": {"gtin_type": GTIN_TYPE.GTIN8},
},
],
):
pass
11 changes: 11 additions & 0 deletions tests/utils/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
GSRNP,
IMOVN,
ITIP,
LGTIN,
PGLN,
SGCN,
SGLN,
Expand Down Expand Up @@ -164,6 +165,16 @@
"hex": "41000B7F7070D40000830A1C48B1A3C8932A5CC9B3A7D05F4A9D4000",
"binary": "01000001000000000000101101111111011100000111000011010100000000000000000010000011000010100001110001001000101100011010001111001000100100110010101001011100110010011011001110100111110100000101111101001010100111010100000000000000",
},
{
"scheme": LGTIN,
"tag_encodable": False,
"gs1_keyed": True,
"gs1_element": True,
"uri": "urn:epc:class:lgtin:50712192365.88..%25:.13%26",
"gs1_key": "85071219236581",
"gs1_element_string": "(01)85071219236581(10).%:.13&",
"company_prefix_length": 11,
},
{
"scheme": PGLN,
"tag_encodable": False,
Expand Down

0 comments on commit f5a454f

Please sign in to comment.