From ea8baa48dea1c84199097eda85349c47d4fee46d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Aug 2020 10:44:56 +0200 Subject: [PATCH] gs1: Add methods to find parsed Element Strings --- README.md | 3 ++ docs/changes.rst | 6 +++ src/biip/gs1/__init__.py | 4 +- src/biip/gs1/_messages.py | 61 +++++++++++++++++++++++- tests/gs1/test_messages.py | 98 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9ee61af..524ff31 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ The library can interpret the following formats: parse amounts with currency into `Money` values - [x] Encode as Human Readable Interpretation (HRI), e.g. with parenthesis around the AI numbers + - [x] Easy lookup of parsed Element Strings by: + - [x] AI prefix + - [x] Part of AI's data title - GTIN (Global Trade Item Number) - [x] Parse GTIN-8, e.g. from EAN-8 barcodes - [x] Parse GTIN-12, e.g. from UPC-A and UPC-E barcodes diff --git a/docs/changes.rst b/docs/changes.rst index 2af09d9..a16afc8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -7,6 +7,12 @@ v0.3.0 (UNRELEASED) :mod:`biip.gs1` +- Add :meth:`~biip.gs1.GS1Message.filter` to find all parsed Element Strings + that matches the criteria. + +- Add :meth:`~biip.gs1.GS1Message.get` to find first parsed Element String + that matches the criteria. + - Add :attr:`~biip.gs1.GS1ElementString.decimal` field which is set for AIs with weight, volume, dimensions, dicount percentages, and amounts payable. diff --git a/src/biip/gs1/__init__.py b/src/biip/gs1/__init__.py index 5490864..3576c5f 100644 --- a/src/biip/gs1/__init__.py +++ b/src/biip/gs1/__init__.py @@ -28,13 +28,13 @@ format=GtinFormat.GTIN_13, prefix=GS1Prefix(value='703', usage='GS1 Norway'), payload='703206980498', check_digit=8, packaging_level=None), date=None, decimal=None, money=None) - >>> msg.element_strings[1] + >>> msg.get(data_title='BEST BY') GS1ElementString(ai=GS1ApplicationIdentifier(ai='15', description='Best before date (YYMMDD)', data_title='BEST BEFORE or BEST BY', fnc1_required=False, format='N2+N6'), value='210526', pattern_groups=['210526'], gtin=None, date=datetime.date(2021, 5, 26), decimal=None, money=None) - >>> msg.element_strings[2] + >>> msg.get(ai="10") GS1ElementString(ai=GS1ApplicationIdentifier(ai='10', description='Batch or lot number', data_title='BATCH/LOT', fnc1_required=True, format='N2+X..20'), value='0329', pattern_groups=['0329'], gtin=None, diff --git a/src/biip/gs1/_messages.py b/src/biip/gs1/_messages.py index c798cf9..ba263bd 100644 --- a/src/biip/gs1/_messages.py +++ b/src/biip/gs1/_messages.py @@ -3,10 +3,14 @@ from __future__ import annotations from dataclasses import dataclass -from typing import List, Type +from typing import List, Optional, Type, Union from biip import ParseError -from biip.gs1 import DEFAULT_SEPARATOR_CHAR, GS1ElementString +from biip.gs1 import ( + DEFAULT_SEPARATOR_CHAR, + GS1ApplicationIdentifier, + GS1ElementString, +) @dataclass @@ -80,3 +84,56 @@ def as_hri(self: GS1Message) -> str: A human-readable string where the AIs are wrapped in parenthesis. """ return "".join(es.as_hri() for es in self.element_strings) + + def filter( + self: GS1Message, + *, + ai: Optional[Union[str, GS1ApplicationIdentifier]] = None, + data_title: Optional[str] = None, + ) -> List[GS1ElementString]: + """Filter Element Strings by AI or data title. + + Args: + ai: AI instance or string to match against the start of the + Element String's AI. + data_title: String to find anywhere in the Element String's AI + data title. + + Returns: + All matching Element Strings in the message. + """ + if isinstance(ai, GS1ApplicationIdentifier): + ai = ai.ai + + result = [] + + for element_string in self.element_strings: + if ai is not None and element_string.ai.ai.startswith(ai): + result.append(element_string) + elif ( + data_title is not None + and data_title in element_string.ai.data_title + ): + result.append(element_string) + + return result + + def get( + self: GS1Message, + *, + ai: Optional[Union[str, GS1ApplicationIdentifier]] = None, + data_title: Optional[str] = None, + ) -> Optional[GS1ElementString]: + """Get Element String by AI or data title. + + Args: + ai: AI instance or string to match against the start of the + Element String's AI. + data_title: String to find anywhere in the Element String's AI + data title.. + + Returns: + The first matching Element String in the message. + """ + matches = self.filter(ai=ai, data_title=data_title) + return matches[0] if matches else None diff --git a/tests/gs1/test_messages.py b/tests/gs1/test_messages.py index b9efa5d..6266735 100644 --- a/tests/gs1/test_messages.py +++ b/tests/gs1/test_messages.py @@ -1,4 +1,5 @@ from datetime import date +from typing import List import pytest @@ -193,3 +194,100 @@ def test_parse_fails_if_fixed_length_field_ends_with_separator_char() -> None: ) def test_as_hri(value: str, expected: str) -> None: assert GS1Message.parse(value).as_hri() == expected + + +@pytest.mark.parametrize( + "value, ai, expected", + [ + ("010703206980498815210526100329", "01", ["07032069804988"]), + ("010703206980498815210526100329", "15", ["210526"]), + ("010703206980498815210526100329", "37", []), + ("7230EM123\x1d7231EM456\x1d7232EM789", "7231", ["EM456"]), + ( + "7230EM123\x1d7231EM456\x1d7232EM789", + "723", + ["EM123", "EM456", "EM789"], + ), + ], +) +def test_filter_element_strings_by_ai( + value: str, ai: str, expected: List[str] +) -> None: + matches = GS1Message.parse(value).filter(ai=ai) + + assert [element_string.value for element_string in matches] == expected + + +@pytest.mark.parametrize( + "value, data_title, expected", + [ + ("010703206980498815210526100329", "GTIN", ["07032069804988"]), + ("010703206980498815210526100329", "BEST BY", ["210526"]), + ("010703206980498815210526100329", "COUNT", []), + ( + "7230EM123\x1d7231EM456\x1d7232EM789", + "CERT", + ["EM123", "EM456", "EM789"], + ), + ], +) +def test_filter_element_strings_by_data_title( + value: str, data_title: str, expected: List[str] +) -> None: + matches = GS1Message.parse(value).filter(data_title=data_title) + + assert [element_string.value for element_string in matches] == expected + + +@pytest.mark.parametrize( + "value, ai, expected", + [ + ("010703206980498815210526100329", "01", "07032069804988"), + ("010703206980498815210526100329", "15", "210526"), + ("010703206980498815210526100329", "10", "0329"), + ("010703206980498815210526100329", "37", None), + ("7230EM123\x1d7231EM456\x1d7232EM789", "7231", "EM456"), + ("7230EM123\x1d7231EM456\x1d7232EM789", "723", "EM123"), + ], +) +def test_get_element_string_by_ai(value: str, ai: str, expected: str) -> None: + element_string = GS1Message.parse(value).get(ai=ai) + + if expected is None: + assert element_string is None + else: + assert element_string is not None + assert element_string.value == expected + + +@pytest.mark.parametrize( + "value, data_title, expected", + [ + ("010703206980498815210526100329", "GTIN", "07032069804988"), + ("010703206980498815210526100329", "BEST BY", "210526"), + ("010703206980498815210526100329", "BATCH", "0329"), + ("010703206980498815210526100329", "COUNT", None), + ("7230EM123\x1d7231EM456\x1d7232EM789", "CERT #2", "EM456"), + ("7230EM123\x1d7231EM456\x1d7232EM789", "CERT", "EM123"), + ], +) +def test_get_element_string_by_data_title( + value: str, data_title: str, expected: str +) -> None: + element_string = GS1Message.parse(value).get(data_title=data_title) + + if expected is None: + assert element_string is None + else: + assert element_string is not None + assert element_string.value == expected + + +def test_filter_element_strings_by_ai_instance() -> None: + ai = GS1ApplicationIdentifier.extract("01") + msg = GS1Message.parse("010703206980498815210526100329") + + element_string = msg.get(ai=ai) + + assert element_string is not None + assert element_string.value == "07032069804988"