lists.arthurdejong.org
RSS feed

python-stdnum branch master updated. 1.13-42-g180788a

[Date Prev][Date Next] [Thread Prev][Thread Next]

python-stdnum branch master updated. 1.13-42-g180788a



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-stdnum".

The branch, master has been updated
       via  180788af207f394e38b458ad14fb68f4853f4a9a (commit)
      from  c2284f322679e9527794aaa81e8fbb57792c5a21 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-stdnum/commit/?id=180788af207f394e38b458ad14fb68f4853f4a9a

commit 180788af207f394e38b458ad14fb68f4853f4a9a
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Aug 8 15:44:08 2020 +0200

    Add GS1-128 format
    
    This adds validation, parsing and encoding functions for GS1-128. It is
    based on the lists of formats as published by the GS1 organisation.
    
    Based on the implementation provided by Sergi Almacellas Abellana
    <sergi@koolpi.com>.
    
    Closes https://github.com/arthurdejong/python-stdnum/pull/144

diff --git a/stdnum/gs1_128.py b/stdnum/gs1_128.py
new file mode 100644
index 0000000..aee11d2
--- /dev/null
+++ b/stdnum/gs1_128.py
@@ -0,0 +1,269 @@
+# gs1_128.py - functions for handling GS1-128 codes
+#
+# Copyright (C) 2019 Sergi Almacellas Abellana
+# Copyright (C) 2020 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""GS1-128 (Standard to encode product information in Code 128 barcodes).
+
+The GS1-128 (also called EAN-128, UCC/EAN-128 or UCC-128) is an international
+standard for embedding data such as best before dates, weights, etc. with
+Application Identifiers (AI).
+
+The GS1-128 standard is used as a product identification code on bar codes.
+It embeds data with Application Identifiers (AI) that defines the kind of
+data, the type and length. The standard is also known as UCC/EAN-128, UCC-128
+and EAN-128.
+
+GS1-128 is a subset of Code 128 symbology.
+
+More information:
+
+* https://en.wikipedia.org/wiki/GS1-128
+* https://www.gs1.org/standards/barcodes/application-identifiers
+* https://www.gs1.org/docs/barcodes/GS1_General_Specifications.pdf
+
+>>> compact('(01)38425876095074(17)181119(37)1 ')
+'013842587609507417181119371'
+>>> encode({'01': '38425876095074'})
+'0138425876095074'
+>>> info('0138425876095074')
+{'01': '38425876095074'}
+>>> validate('(17)181119(01)38425876095074(37)1')
+'013842587609507417181119371'
+"""
+
+import datetime
+import decimal
+import re
+
+from stdnum import numdb
+from stdnum.exceptions import *
+from stdnum.util import clean
+
+
+# our open copy of the application identifier database
+_gs1_aidb = numdb.get('gs1_ai')
+
+
+# Extra validation modules based on the application identifier
+_ai_validators = {
+    '01': 'stdnum.ean',
+    '02': 'stdnum.ean',
+    '8007': 'stdnum.iban',
+}
+
+
+def compact(number):
+    """Convert the GS1-128 to the minimal representation.
+
+    This strips the number of any valid separators and removes surrounding
+    whitespace. For a more consistent compact representation use
+    :func:`validate()`.
+    """
+    return clean(number, '()').strip()
+
+
+def _encode_value(fmt, _type, value):
+    """Encode the specified value given the format and type."""
+    if _type == 'decimal':
+        if isinstance(value, (list, tuple)) and fmt.startswith('N3+'):
+            number = _encode_value(fmt[3:], _type, value[1])
+            return number[0] + value[0].rjust(3, '0') + number[1:]
+        value = str(value)
+        if fmt.startswith('N..'):
+            length = int(fmt[3:])
+            value = value[:length + 1]
+            number, digits = (value.split('.') + [''])[:2]
+            digits = digits[:9]
+            return str(len(digits)) + number + digits
+        else:
+            length = int(fmt[1:])
+            value = value[:length + 1]
+            number, digits = (value.split('.') + [''])[:2]
+            digits = digits[:9]
+            return str(len(digits)) + (number + digits).rjust(length, '0')
+    elif _type == 'date':
+        if isinstance(value, (list, tuple)) and fmt == 'N6..12':
+            return '%s%s' % (
+                _encode_value('N6', _type, value[0]),
+                _encode_value('N6', _type, value[1]))
+        elif isinstance(value, datetime.date):
+            if fmt == 'N10':
+                return value.strftime('%y%m%d%H%M')
+            elif fmt == 'N8+N..4':
+                value = datetime.datetime.strftime(value, '%y%m%d%H%M%S')
+                if value.endswith('00'):
+                    value = value[:-2]
+                if value.endswith('00'):
+                    value = value[:-2]
+                return value
+            return value.strftime('%y%m%d')
+    return str(value)
+
+
+def _max_length(fmt, _type):
+    """Determine the maximum length based on the format ad type."""
+    length = sum(int(re.match(r'^[NXY][0-9]*?[.]*([0-9]+)$', x).group(1)) for 
x in fmt.split('+'))
+    if _type == 'decimal':
+        length += 1
+    return length
+
+
+def _pad_value(fmt, _type, value):
+    """Pad the value to the maximum length for the format."""
+    if _type in ('decimal', 'int'):
+        return value.rjust(_max_length(fmt, _type), '0')
+    return value.ljust(_max_length(fmt, _type))
+
+
+def _decode_value(fmt, _type, value):
+    """Decode the specified value given the fmt and type."""
+    if _type == 'decimal':
+        if fmt.startswith('N3+'):
+            return (value[1:4], _decode_value(fmt[3:], _type, value[0] + 
value[4:]))
+        digits = int(value[0])
+        value = value[1:]
+        if digits:
+            value = value[:-digits] + '.' + value[-digits:]
+        return decimal.Decimal(value)
+    elif _type == 'date':
+        if fmt == 'N8+N..4':
+            return datetime.datetime.strptime(value, 
'%y%m%d%H%M%S'[:len(value)])
+        elif len(value) == 10:
+            return datetime.datetime.strptime(value, '%y%m%d%H%M')
+        elif len(value) == 12:
+            return (_decode_value(fmt, _type, value[:6]), _decode_value(fmt, 
_type, value[6:]))
+        return datetime.datetime.strptime(value, '%y%m%d').date()
+    elif _type == 'int':
+        return int(value)
+    return value.strip()
+
+
+def info(number, separator=''):
+    """Return a dictionary containing the information from the GS1-128 code.
+
+    The returned dictionary maps application identifiers to values with the
+    appropriate type (`str`, `int`, `Decimal`, `datetime.date` or
+    `datetime.datetime`).
+
+    If a `separator` is provided it will be used as FNC1 to determine the end
+    of variable-sized values.
+    """
+    number = compact(number)
+    data = {}
+    identifier = ''
+    # skip separator
+    if separator and number.startswith(separator):
+        number = number[len(separator):]
+    while number:
+        # extract the application identifier
+        ai, info = _gs1_aidb.info(number)[0]
+        if not info or not number.startswith(ai):
+            raise InvalidComponent()
+        number = number[len(ai):]
+        # figure out the value part
+        value = number[:_max_length(info['format'], info['type'])]
+        if separator and info.get('fnc1', False):
+            idx = number.find(separator)
+            if idx > 0:
+                value = number[:idx]
+        number = number[len(value):]
+        # validate the value if we have a custom module for it
+        if ai in _ai_validators:
+            mod = __import__(_ai_validators[ai], globals(), locals(), 
['validate'])
+            mod.validate(value)
+        # convert the number
+        data[ai] = _decode_value(info['format'], info['type'], value)
+        # skip separator
+        if separator and number.startswith(separator):
+            number = number[len(separator):]
+    return data
+
+
+def encode(data, separator='', parentheses=False):
+    """Generate a GS1-128 for the application identifiers supplied.
+
+    The provided dictionary is expected to map application identifiers to
+    values. The supported value types and formats depend on the application
+    identifier.
+
+    If a `separator` is provided it will be used as FNC1 representation,
+    otherwise variable-sized values will be expanded to their maximum size
+    with appropriate padding.
+
+    If `parentheses` is set the application identifiers will be surrounded
+    by parentheses for readability.
+    """
+    ai_fmt = '(%s)' if parentheses else '%s'
+    # we keep items sorted and keep fixed-sized values separate tot output
+    # them first
+    fixed_values = []
+    variable_values = []
+    for inputai, value in sorted(data.items()):
+        ai, info = _gs1_aidb.info(inputai)[0]
+        if not info:
+            raise InvalidComponent()
+        # validate the value if we have a custom module for it
+        if ai in _ai_validators:
+            mod = __import__(_ai_validators[ai], globals(), locals(), 
['validate'])
+            mod.validate(value)
+        value = _encode_value(info['format'], info['type'], value)
+        # store variable-sized values separate from fixed-size values
+        if info.get('fnc1', False):
+            variable_values.append((ai_fmt % ai, info['format'], info['type'], 
value))
+        else:
+            fixed_values.append(ai_fmt % ai + value)
+    # we need the separator for all but the last variable-sized value
+    # (or pad values if we don't have a separator)
+    return ''.join(
+        fixed_values + [
+            ai + (value if separator else _pad_value(fmt, _type, value)) + 
separator
+            for ai, fmt, _type, value in variable_values[:-1]
+        ] + [
+            ai + value
+            for ai, fmt, _type, value in variable_values[-1:]
+        ])
+
+
+def validate(number, separator=''):
+    """Check if the number provided is a valid GS1-128.
+
+    This checks formatting of the number and values and returns a stable
+    representation.
+
+    If a separator is provided it will be used as FNC1 for both parsing the
+    provided number and for encoding the returned number.
+    """
+    try:
+        return encode(info(number, separator), separator)
+    except ValidationError:
+        raise
+    except Exception:
+        # We wrap all other exceptions to ensure that we only return
+        # exceptions that are a subclass of ValidationError
+        # (the info() and encode() functions expect some semblance of valid
+        # input)
+        raise InvalidFormat()
+
+
+def is_valid(number, separator=''):
+    """Check if the number provided is a valid GS1-128."""
+    try:
+        return bool(validate(number))
+    except ValidationError:
+        return False
diff --git a/stdnum/gs1_ai.dat b/stdnum/gs1_ai.dat
new file mode 100644
index 0000000..0b1ae6c
--- /dev/null
+++ b/stdnum/gs1_ai.dat
@@ -0,0 +1,170 @@
+# generated from https://www.gs1.org/standards/barcodes/application-identifiers
+# on 2020-07-12 19:36:00.576283
+00 format="N18" type="str" name="SSCC" description="Serial Shipping Container 
Code (SSCC)"
+01 format="N14" type="str" name="GTIN" description="Global Trade Item Number 
(GTIN)"
+02 format="N14" type="str" name="CONTENT" description="GTIN of contained trade 
items"
+10 format="X..20" type="str" fnc1="1" name="BATCH/LOT" description="Batch or 
lot number"
+11 format="N6" type="date" name="PROD DATE" description="Production date 
(YYMMDD)"
+12 format="N6" type="date" name="DUE DATE" description="Due date (YYMMDD)"
+13 format="N6" type="date" name="PACK DATE" description="Packaging date 
(YYMMDD)"
+15 format="N6" type="date" name="BEST BEFORE or BEST BY" description="Best 
before date (YYMMDD)"
+16 format="N6" type="date" name="SELL BY" description="Sell by date (YYMMDD)"
+17 format="N6" type="date" name="USE BY OR EXPIRY" description="Expiration 
date (YYMMDD)"
+20 format="N2" type="str" name="VARIANT" description="Internal product variant"
+21 format="X..20" type="str" fnc1="1" name="SERIAL" description="Serial number"
+22 format="X..20" type="str" fnc1="1" name="CPV" description="Consumer product 
variant"
+235 format="X..28" type="str" fnc1="1" name="TPX" description="Third Party 
Controlled, Serialised Extension of GTIN (TPX)"
+240 format="X..30" type="str" fnc1="1" name="ADDITIONAL ID" 
description="Additional product identification assigned by the manufacturer"
+241 format="X..30" type="str" fnc1="1" name="CUST. PART NO." 
description="Customer part number"
+242 format="N..6" type="str" fnc1="1" name="MTO VARIANT" 
description="Made-to-Order variation number"
+243 format="X..20" type="str" fnc1="1" name="PCN" description="Packaging 
component number"
+250 format="X..30" type="str" fnc1="1" name="SECONDARY SERIAL" 
description="Secondary serial number"
+251 format="X..30" type="str" fnc1="1" name="REF. TO SOURCE" 
description="Reference to source entity"
+253 format="N13+X..17" type="str" fnc1="1" name="GDTI" description="Global 
Document Type Identifier (GDTI)"
+254 format="X..20" type="str" fnc1="1" name="GLN EXTENSION COMPONENT" 
description="GLN extension component"
+255 format="N13+N..12" type="str" fnc1="1" name="GCN" description="Global 
Coupon Number (GCN)"
+30 format="N..8" type="int" fnc1="1" name="VAR. COUNT" description="Variable 
count of items (variable measure trade item)"
+310 format="N6" type="decimal" name="NET WEIGHT (kg)" description="Net weight, 
kilograms (variable measure trade item)"
+311 format="N6" type="decimal" name="LENGTH (m)" description="Length or first 
dimension, metres (variable measure trade item)"
+312 format="N6" type="decimal" name="WIDTH (m)" description="Width, diameter, 
or second dimension, metres (variable measure trade item)"
+313 format="N6" type="decimal" name="HEIGHT (m)" description="Depth, 
thickness, height, or third dimension, metres (variable measure trade item)"
+314 format="N6" type="decimal" name="AREA (m<sup>2</sup>)" description="Area, 
square metres (variable measure trade item)"
+315 format="N6" type="decimal" name="NET VOLUME (l)" description="Net volume, 
litres (variable measure trade item)"
+316 format="N6" type="decimal" name="NET VOLUME (m<sup>3</sup>)" 
description="Net volume, cubic metres (variable measure trade item)"
+320 format="N6" type="decimal" name="NET WEIGHT (lb)" description="Net weight, 
pounds (variable measure trade item)"
+321 format="N6" type="decimal" name="LENGTH (in)" description="Length or first 
dimension, inches (variable measure trade item)"
+322 format="N6" type="decimal" name="LENGTH (ft)" description="Length or first 
dimension, feet (variable measure trade item)"
+323 format="N6" type="decimal" name="LENGTH (yd)" description="Length or first 
dimension, yards (variable measure trade item)"
+324 format="N6" type="decimal" name="WIDTH (in)" description="Width, diameter, 
or second dimension, inches (variable measure trade item)"
+325 format="N6" type="decimal" name="WIDTH (ft)" description="Width, diameter, 
or second dimension, feet (variable measure trade item)"
+326 format="N6" type="decimal" name="WIDTH (yd)" description="Width, diameter, 
or second dimension, yards (variable measure trade item)"
+327 format="N6" type="decimal" name="HEIGHT (in)" description="Depth, 
thickness, height, or third dimension, inches (variable measure trade item)"
+328 format="N6" type="decimal" name="HEIGHT (ft)" description="Depth, 
thickness, height, or third dimension, feet (variable measure trade item)"
+329 format="N6" type="decimal" name="HEIGHT (yd)" description="Depth, 
thickness, height, or third dimension, yards (variable measure trade item)"
+330 format="N6" type="decimal" name="GROSS WEIGHT (kg)" description="Logistic 
weight, kilograms"
+331 format="N6" type="decimal" name="LENGTH (m), log" description="Length or 
first dimension, metres"
+332 format="N6" type="decimal" name="WIDTH (m), log" description="Width, 
diameter, or second dimension, metres"
+333 format="N6" type="decimal" name="HEIGHT (m), log" description="Depth, 
thickness, height, or third dimension, metres"
+334 format="N6" type="decimal" name="AREA (m<sup>2</sup>), log" 
description="Area, square metres"
+335 format="N6" type="decimal" name="VOLUME (l), log" description="Logistic 
volume, litres"
+336 format="N6" type="decimal" name="VOLUME (m<sup>3</sup>), log" 
description="Logistic volume, cubic metres"
+337 format="N6" type="decimal" name="KG PER m<sup>2</sup>" 
description="Kilograms per square metre"
+340 format="N6" type="decimal" name="GROSS WEIGHT (lb)" description="Logistic 
weight, pounds"
+341 format="N6" type="decimal" name="LENGTH (in), log" description="Length or 
first dimension, inches"
+342 format="N6" type="decimal" name="LENGTH (ft), log" description="Length or 
first dimension, feet"
+343 format="N6" type="decimal" name="LENGTH (yd), log" description="Length or 
first dimension, yards"
+344 format="N6" type="decimal" name="WIDTH (in), log" description="Width, 
diameter, or second dimension, inches"
+345 format="N6" type="decimal" name="WIDTH (ft), log" description="Width, 
diameter, or second dimension, feet"
+346 format="N6" type="decimal" name="WIDTH (yd), log" description="Width, 
diameter, or second dimension, yard"
+347 format="N6" type="decimal" name="HEIGHT (in), log" description="Depth, 
thickness, height, or third dimension, inches"
+348 format="N6" type="decimal" name="HEIGHT (ft), log" description="Depth, 
thickness, height, or third dimension, feet"
+349 format="N6" type="decimal" name="HEIGHT (yd), log" description="Depth, 
thickness, height, or third dimension, yards"
+350 format="N6" type="decimal" name="AREA (in<sup>2</sup>)" description="Area, 
square inches (variable measure trade item)"
+351 format="N6" type="decimal" name="AREA (ft<sup>2</sup>)" description="Area, 
square feet (variable measure trade item)"
+352 format="N6" type="decimal" name="AREA (yd<sup>2</sup>)" description="Area, 
square yards (variable measure trade item)"
+353 format="N6" type="decimal" name="AREA (in<sup>2</sup>), log" 
description="Area, square inches"
+354 format="N6" type="decimal" name="AREA (ft<sup>2</sup>), log" 
description="Area, square feet"
+355 format="N6" type="decimal" name="AREA (yd<sup>2</sup>), log" 
description="Area, square yards"
+356 format="N6" type="decimal" name="NET WEIGHT (t oz)" description="Net 
weight, troy ounces (variable measure trade item)"
+357 format="N6" type="decimal" name="NET VOLUME (oz)" description="Net weight 
(or volume), ounces (variable measure trade item)"
+360 format="N6" type="decimal" name="NET VOLUME (qt)" description="Net volume, 
quarts (variable measure trade item)"
+361 format="N6" type="decimal" name="NET VOLUME (gal.)" description="Net 
volume, gallons U.S. (variable measure trade item)"
+362 format="N6" type="decimal" name="VOLUME (qt), log" description="Logistic 
volume, quarts"
+363 format="N6" type="decimal" name="VOLUME (gal.), log" description="Logistic 
volume, gallons U.S."
+364 format="N6" type="decimal" name="VOLUME (in<sup>3</sup>)" description="Net 
volume, cubic inches (variable measure trade item)"
+365 format="N6" type="decimal" name="VOLUME (ft<sup>3</sup>)" description="Net 
volume, cubic feet (variable measure trade item)"
+366 format="N6" type="decimal" name="VOLUME (yd<sup>3</sup>)" description="Net 
volume, cubic yards (variable measure trade item)"
+367 format="N6" type="decimal" name="VOLUME (in<sup>3</sup>), log" 
description="Logistic volume, cubic inches"
+368 format="N6" type="decimal" name="VOLUME (ft<sup>3</sup>), log" 
description="Logistic volume, cubic feet"
+369 format="N6" type="decimal" name="VOLUME (yd<sup>3</sup>), log" 
description="Logistic volume, cubic yards"
+37 format="N..8" type="int" fnc1="1" name="COUNT" description="Count of trade 
items or trade item pieces contained in a logistic unit"
+390 format="N..15" type="decimal" fnc1="1" name="AMOUNT" 
description="Applicable amount payable or Coupon value, local currency"
+391 format="N3+N..15" type="decimal" fnc1="1" name="AMOUNT" 
description="Applicable amount payable with ISO currency code"
+392 format="N..15" type="decimal" fnc1="1" name="PRICE" 
description="Applicable amount payable, single monetary area (variable measure 
trade item)"
+393 format="N3+N..15" type="decimal" fnc1="1" name="PRICE" 
description="Applicable amount payable with ISO currency code (variable measure 
trade item)"
+394 format="N4" type="decimal" fnc1="1" name="PRCNT OFF" 
description="Percentage discount of a coupon"
+400 format="X..30" type="str" fnc1="1" name="ORDER NUMBER" 
description="Customers purchase order number"
+401 format="X..30" type="str" fnc1="1" name="GINC" description="Global 
Identification Number for Consignment (GINC)"
+402 format="N17" type="str" fnc1="1" name="GSIN" description="Global Shipment 
Identification Number (GSIN)"
+403 format="X..30" type="str" fnc1="1" name="ROUTE" description="Routing code"
+410 format="N13" type="str" name="SHIP TO LOC" description="Ship to - Deliver 
to Global Location Number"
+411 format="N13" type="str" name="BILL TO" description="Bill to - Invoice to 
Global Location Number"
+412 format="N13" type="str" name="PURCHASE FROM" description="Purchased from 
Global Location Number"
+413 format="N13" type="str" name="SHIP FOR LOC" description="Ship for - 
Deliver for - Forward to Global Location Number"
+414 format="N13" type="str" name="LOC No" description="Identification of a 
physical location - Global Location Number"
+415 format="N13" type="str" name="PAY TO" description="Global Location Number 
of the invoicing party"
+416 format="N13" type="str" name="PROD/SERV LOC" description="GLN of the 
production or service location"
+417 format="N13" type="str" name="PARTY" description="Party GLN"
+420 format="X..20" type="str" fnc1="1" name="SHIP TO POST" description="Ship 
to - Deliver to postal code within a single postal authority"
+421 format="N3+X..9" type="str" fnc1="1" name="SHIP TO POST" description="Ship 
to - Deliver to postal code with ISO country code"
+422 format="N3" type="int" fnc1="1" name="ORIGIN" description="Country of 
origin of a trade item"
+423 format="N3+N..12" type="str" fnc1="1" name="COUNTRY - INITIAL PROCESS." 
description="Country of initial processing"
+424 format="N3" type="int" fnc1="1" name="COUNTRY - PROCESS." 
description="Country of processing"
+425 format="N3+N..12" type="str" fnc1="1" name="COUNTRY - DISASSEMBLY" 
description="Country of disassembly"
+426 format="N3" type="int" fnc1="1" name="COUNTRY - FULL PROCESS" 
description="Country covering full process chain"
+427 format="X..3" type="str" fnc1="1" name="ORIGIN SUBDIVISION" 
description="Country subdivision Of origin"
+7001 format="N13" type="str" fnc1="1" name="NSN" description="NATO Stock 
Number (NSN)"
+7002 format="X..30" type="str" fnc1="1" name="MEAT CUT" description="UN/ECE 
meat carcasses and cuts classification"
+7003 format="N10" type="date" fnc1="1" name="EXPIRY TIME" 
description="Expiration date and time"
+7004 format="N..4" type="str" fnc1="1" name="ACTIVE POTENCY" 
description="Active potency"
+7005 format="X..12" type="str" fnc1="1" name="CATCH AREA" description="Catch 
area"
+7006 format="N6" type="date" fnc1="1" name="FIRST FREEZE DATE" 
description="First freeze date"
+7007 format="N6..12" type="date" fnc1="1" name="HARVEST DATE" 
description="Harvest date"
+7008 format="X..3" type="str" fnc1="1" name="AQUATIC SPECIES" 
description="Species for fishery purposes"
+7009 format="X..10" type="str" fnc1="1" name="FISHING GEAR TYPE" 
description="Fishing gear type"
+7010 format="X..2" type="str" fnc1="1" name="PROD METHOD" 
description="Production method"
+7020 format="X..20" type="str" fnc1="1" name="REFURB LOT" 
description="Refurbishment lot ID"
+7021 format="X..20" type="str" fnc1="1" name="FUNC STAT" 
description="Functional status"
+7022 format="X..20" type="str" fnc1="1" name="REV STAT" description="Revision 
status"
+7023 format="X..30" type="str" fnc1="1" name="GIAI - ASSEMBLY" 
description="Global Individual Asset Identifier (GIAI) of an assembly"
+7030 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 0" 
description="Number of processor with ISO Country Code"
+7031 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 1" 
description="Number of processor with ISO Country Code"
+7032 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 2" 
description="Number of processor with ISO Country Code"
+7033 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 3" 
description="Number of processor with ISO Country Code"
+7034 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 4" 
description="Number of processor with ISO Country Code"
+7035 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 5" 
description="Number of processor with ISO Country Code"
+7036 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 6" 
description="Number of processor with ISO Country Code"
+7037 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 7" 
description="Number of processor with ISO Country Code"
+7038 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 8" 
description="Number of processor with ISO Country Code"
+7039 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 9" 
description="Number of processor with ISO Country Code"
+7040 format="N1+X3" type="str" fnc1="1" name="UIC+EXT" description="GS1 UIC 
with Extension 1 and Importer index"
+710 format="X..20" type="str" fnc1="1" name="NHRN PZN" description="National 
Healthcare Reimbursement Number (NHRN) - Germany PZN"
+711 format="X..20" type="str" fnc1="1" name="NHRN CIP" description="National 
Healthcare Reimbursement Number (NHRN) - France CIP"
+712 format="X..20" type="str" fnc1="1" name="NHRN CN" description="National 
Healthcare Reimbursement Number (NHRN) - Spain CN"
+713 format="X..20" type="str" fnc1="1" name="NHRN DRN" description="National 
Healthcare Reimbursement Number (NHRN) - Brasil DRN"
+714 format="X..20" type="str" fnc1="1" name="NHRN AIM" description="National 
Healthcare Reimbursement Number (NHRN) - Portugal AIM"
+7230 format="X2+X..28" type="str" fnc1="1" name="CERT #1" 
description="Certification reference"
+7231 format="X2+X..28" type="str" fnc1="1" name="CERT #2" 
description="Certification reference"
+7232 format="X2+X..28" type="str" fnc1="1" name="CERT #3" 
description="Certification reference"
+7233 format="X2+X..28" type="str" fnc1="1" name="CERT #4" 
description="Certification reference"
+7234 format="X2+X..28" type="str" fnc1="1" name="CERT #5" 
description="Certification reference"
+7235 format="X2+X..28" type="str" fnc1="1" name="CERT #6" 
description="Certification reference"
+7236 format="X2+X..28" type="str" fnc1="1" name="CERT #7" 
description="Certification reference"
+7237 format="X2+X..28" type="str" fnc1="1" name="CERT #8" 
description="Certification reference"
+7238 format="X2+X..28" type="str" fnc1="1" name="CERT #9" 
description="Certification reference"
+7239 format="X2+X..28" type="str" fnc1="1" name="CERT #10" 
description="Certification reference"
+7240 format="X..20" type="str" fnc1="1" name="PROTOCOL" description="Protocol 
ID"
+8001 format="N14" type="str" fnc1="1" name="DIMENSIONS" description="Roll 
products (width, length, core diameter, direction, splices)"
+8002 format="X..20" type="str" fnc1="1" name="CMT No" description="Cellular 
mobile telephone identifier"
+8003 format="N14+X..16" type="str" fnc1="1" name="GRAI" description="Global 
Returnable Asset Identifier (GRAI)"
+8004 format="X..30" type="str" fnc1="1" name="GIAI" description="Global 
Individual Asset Identifier (GIAI)"
+8005 format="N6" type="str" fnc1="1" name="PRICE PER UNIT" description="Price 
per unit of measure"
+8006 format="N14+N2+N2" type="str" fnc1="1" name="ITIP" 
description="Identification of an individual trade item piece"
+8007 format="X..34" type="str" fnc1="1" name="IBAN" description="International 
Bank Account Number (IBAN)"
+8008 format="N8+N..4" type="date" fnc1="1" name="PROD TIME" description="Date 
and time of production"
+8009 format="X..50" type="str" fnc1="1" name="OPTSEN" description="Optically 
Readable Sensor Indicator"
+8010 format="Y..30" type="str" fnc1="1" name="CPID" 
description="Component/Part Identifier (CPID)"
+8011 format="N..12" type="str" fnc1="1" name="CPID SERIAL" 
description="Component/Part Identifier serial number (CPID SERIAL)"
+8012 format="X..20" type="str" fnc1="1" name="VERSION" description="Software 
version"
+8013 format="X..30" type="str" fnc1="1" name="GMN (for medical devices, the 
default, global data title is BUDI-DI)" description="Global Model Number (GMN)"
+8017 format="N18" type="str" fnc1="1" name="GSRN - PROVIDER" 
description="Global Service Relation Number to identify the relationship 
between an organisation offering services and the provider of services"
+8018 format="N18" type="str" fnc1="1" name="GSRN - RECIPIENT" 
description="Global Service Relation Number to identify the relationship 
between an organisation offering services and the recipient of services"
+8019 format="N..10" type="str" fnc1="1" name="SRIN" description="Service 
Relation Instance Number (SRIN)"
+8020 format="X..25" type="str" fnc1="1" name="REF No" description="Payment 
slip reference number"
+8026 format="N14+N2+N2" type="str" fnc1="1" name="ITIP CONTENT" 
description="Identification of pieces of a trade item (ITIP) contained in a 
logistic unit"
+8110 format="X..70" type="str" fnc1="1" name="" description="Coupon code 
identification for use in North America"
+8111 format="N4" type="str" fnc1="1" name="POINTS" description="Loyalty points 
of a coupon"
+8112 format="X..70" type="str" fnc1="1" name="" description="Paperless coupon 
code identification for use in North America"
+8200 format="X..70" type="str" fnc1="1" name="PRODUCT URL" 
description="Extended Packaging URL"
+90 format="X..30" type="str" fnc1="1" name="INTERNAL" description="Information 
mutually agreed between trading partners"
+91-99 format="X..90" type="str" fnc1="1" name="INTERNAL" description="Company 
internal information"
diff --git a/tests/test_gs1_128.doctest b/tests/test_gs1_128.doctest
new file mode 100644
index 0000000..8bfa9af
--- /dev/null
+++ b/tests/test_gs1_128.doctest
@@ -0,0 +1,148 @@
+test_gs1_128.doctest - more detailed doctests for the stdnum.gs1_128 module
+
+Copyright (C) 2019 Sergi Almacellas Abellaan
+Copyright (C) 2020 Arthur de Jong
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA
+
+
+This file contains more detailed doctests for the stdnum.gs1_128 module. It
+tries to test more corner cases and detailed functionality that is not
+really useful as module documentation.
+
+>>> from decimal import Decimal
+>>> import datetime
+>>> import pprint
+>>> from stdnum import gs1_128
+
+
+>>> gs1_128.compact('(01)38425876095074(17)181119(37)1 ')
+'013842587609507417181119371'
+
+
+We can create a GS1-128 code based on data we provide. Various data types
+will be converted to the correct representation.
+
+>>> gs1_128.encode({'01': '38425876095074', '17': datetime.date(2018, 11, 19), 
'37': 1}, parentheses=True)
+'(01)38425876095074(17)181119(37)1'
+>>> gs1_128.encode({'02': '98412345678908', '310': 17.23, '37': 32})
+'029841234567890831020017233732'
+>>> gs1_128.encode({'03': '1234'})  # unknown AI
+Traceback (most recent call last):
+    ...
+InvalidComponent: ...
+
+If we have a separator we use it to separate variable-length values, otherwise
+we pad all variable-length values to the maximum length (except the last one).
+
+>>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '390': 
42, '392': 12}, parentheses=True)
+'(01)58425876097843(10)123456              
(37)00000018(390)0000000000000042(392)012'
+>>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '390': 
42, '392': 12}, parentheses=True, separator='[FNC1]')
+'(01)58425876097843(10)123456[FNC1](37)18[FNC1](390)042[FNC1](392)012'
+
+Numeric values can be provided in several forms and precision is encoded
+properly.
+
+>>> gs1_128.encode({
+...     '310': 17.23,  # float
+...     '311': 456,  # int
+...     '312': 1.0 / 3.0,  # float with lots of digits
+...     '313': '123.456',  # str
+...     '391': ('123', Decimal('123.456')),  # currency number combo
+... }, parentheses=True)
+'(310)2001723(311)0000456(312)5033333(313)3123456(391)3123123456'
+
+We generate dates in various formats, depending on the AI.
+
+>>> gs1_128.encode({
+...     '11': datetime.datetime(2018, 11, 19, 0, 0, 0),
+...     '12': '181119',  # if you provide a string value, it is expected to be 
correct
+...     '7003': datetime.datetime(2018, 11, 19, 12, 45, 13),
+...     '7007': (datetime.date(2018, 11, 19), datetime.date(2018, 11, 21)),
+... }, parentheses=True)
+'(11)181119(12)181119(7003)1811191245(7007)181119181121'
+>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 12, 45, 13)}, 
parentheses=True)
+'(8008)181119124513'
+>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 12, 45)}, 
parentheses=True)
+'(8008)1811191245'
+>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 12, 0)}, 
parentheses=True)
+'(8008)18111912'
+>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 0, 0)}, 
parentheses=True)
+'(8008)18111900'
+
+If we try to encode an invalid EAN we will get an error.
+
+>>> gs1_128.encode({'01': '38425876095079'}, parentheses=True)
+Traceback (most recent call last):
+    ...
+InvalidChecksum: ...
+
+
+We can decode (parse) the GS1-128 code to a dictionary with information about
+the structure of the number.
+
+pprint.pprint(gs1_128.info('(01)38425876095074(17)181119(37)1 '))
+{'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1}
+>>> pprint.pprint(gs1_128.info('013842587609507417181119371'))
+{'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1}
+>>> pprint.pprint(gs1_128.info('(02)98412345678908(310)3017230(37)32'))
+{'02': '98412345678908', '310': Decimal('17.230'), '37': 32}
+>>> pprint.pprint(gs1_128.info('(01)58425876097843(10)123456              
(17)181119(37)18'))
+{'01': '58425876097843', '10': '123456', '17': datetime.date(2018, 11, 19), 
'37': 18}
+>>> 
pprint.pprint(gs1_128.info('|(01)58425876097843|(10)123456|(17)181119(37)18', 
separator='|'))
+{'01': '58425876097843', '10': '123456', '17': datetime.date(2018, 11, 19), 
'37': 18}
+>>> gs1_128.info('(03)38425876095074')  # unknown AI
+Traceback (most recent call last):
+    ...
+InvalidComponent: ...
+
+We can decode decimal values from various formats.
+
+>>> pprint.pprint(gs1_128.info('(310)5033333'))
+{'310': Decimal('0.33333')}
+>>> pprint.pprint(gs1_128.info('(310)0033333'))
+{'310': Decimal('33333')}
+>>> pprint.pprint(gs1_128.info('(391)3123123456'))
+{'391': ('123', Decimal('123.456'))}
+
+We an decode date files from various formats.
+
+>>> pprint.pprint(gs1_128.info('(11)181119'))
+{'11': datetime.date(2018, 11, 19)}
+>>> pprint.pprint(gs1_128.info('(7003)1811191245'))
+{'7003': datetime.datetime(2018, 11, 19, 12, 45)}
+>>> pprint.pprint(gs1_128.info('(7007)181119'))
+{'7007': datetime.date(2018, 11, 19)}
+>>> pprint.pprint(gs1_128.info('(7007)181119181121'))
+{'7007': (datetime.date(2018, 11, 19), datetime.date(2018, 11, 21))}
+>>> pprint.pprint(gs1_128.info('(8008)18111912'))
+{'8008': datetime.datetime(2018, 11, 19, 12, 0)}
+
+
+While the compact() function can clean up the number somewhat the validate()
+function calls info() and then encode() to ensure an even more compact and
+consistent format.
+
+>>> gs1_128.compact('(01)58425876097843(10)123456              (37)00000018')
+'015842587609784310123456              3700000018'
+>>> gs1_128.validate('(01)58425876097843(10)123456              (37)00000018')
+'015842587609784310123456              3718'
+>>> gs1_128.validate('(01)58425876097843(10)123456              (37)00000018', 
separator='|')
+'015842587609784310123456|3718'
+>>> gs1_128.validate('30aa')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
diff --git a/update/gs1_ai.py b/update/gs1_ai.py
new file mode 100755
index 0000000..527a1c5
--- /dev/null
+++ b/update/gs1_ai.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+
+# update/gs1_ai.py - script to get GS1 application identifiers
+#
+# Copyright (C) 2019 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""This script downloads GS1 application identifiers from the GS1 web site."""
+
+import datetime
+import json
+import re
+
+import lxml.html
+import requests
+
+
+# the location of the GS1 application identifiers
+download_url = 'https://www.gs1.org/standards/barcodes/application-identifiers'
+
+
+def fetch_ais():
+    """Download application identifiers frm the GS1 website."""
+    response = requests.get(download_url)
+    document = lxml.html.document_fromstring(response.content)
+    element = document.findall('.//script[@type="application/ld+json"]')[1]
+    for entry in json.loads(element.text)['@graph']:
+        yield (
+            entry['skos:prefLabel'].strip(),             # AI
+            entry['gs1meta:formatAIvalue'].strip()[3:],  # format
+            entry['gs1meta:requiresFNC1'],               # require FNC1
+            [x['@value'] for x in entry['schema:name'] if x['@language'] == 
'en'][0].strip(),
+            [x['@value'] for x in entry['schema:description'] if 
x['@language'] == 'en'][0].strip())
+
+
+def group_ai_ranges():
+    """Combine downloaded application identifiers into ranges."""
+    first = None
+    prev = (None, ) * 5
+    for value in sorted(fetch_ais()):
+        if value[1:] != prev[1:]:
+            if first:
+                yield first, *prev
+            first = value[0]
+        prev = value
+    yield first, *prev
+
+
+if __name__ == '__main__':
+    print('# generated from %s' % download_url)
+    print('# on %s' % datetime.datetime.utcnow())
+    for ai1, ai2, format, require_fnc1, name, description in group_ai_ranges():
+        _type = 'str'
+        if re.match(r'^(N8\+)?N[0-9]*[.]*[0-9]+$', format) and 'date' in 
description.lower():
+            _type = 'date'
+        elif re.match(r'^N[.]*[0-9]+$', format) and 'count' in 
description.lower():
+            _type = 'int'
+        ai = ai1
+        if ai1 != ai2:
+            if len(ai1) == 4:
+                ai = ai1[:3]
+                _type = 'decimal'
+            else:
+                ai = '%s-%s' % (ai1, ai2)
+        print('%s format="%s" type="%s"%s name="%s" description="%s"' % (
+            ai, format, _type,
+            ' fnc1="1"' if require_fnc1 else '',
+            name, description))

-----------------------------------------------------------------------

Summary of changes:
 stdnum/gs1_128.py          | 269 +++++++++++++++++++++++++++++++++++++++++++++
 stdnum/gs1_ai.dat          | 170 ++++++++++++++++++++++++++++
 tests/test_gs1_128.doctest | 148 +++++++++++++++++++++++++
 update/gs1_ai.py           |  82 ++++++++++++++
 4 files changed, 669 insertions(+)
 create mode 100644 stdnum/gs1_128.py
 create mode 100644 stdnum/gs1_ai.dat
 create mode 100644 tests/test_gs1_128.doctest
 create mode 100755 update/gs1_ai.py


hooks/post-receive
-- 
python-stdnum