lists.arthurdejong.org
RSS feed

python-stdnum branch master updated. 2.2-4-g2413a52

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

python-stdnum branch master updated. 2.2-4-g2413a52



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  2413a52f2f62d38be6ae57871d2e370015b085c7 (commit)
      from  72ef1a6befd0ea2ec19df3f3e356a388dc25681e (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=2413a52f2f62d38be6ae57871d2e370015b085c7

commit 2413a52f2f62d38be6ae57871d2e370015b085c7
Author: Jon Arnfred <jonarnfred@gmail.com>
Date:   Fri Jul 17 09:06:27 2026 +0200

    Add support for ISO 4914 UPI validation
    
    Add validation for Unique Product Identifiers, including the QZ prefix,
    restricted alphabet, and ISO 7064 Mod 31, 30 check character.
    
    Closes https://github.com/arthurdejong/python-stdnum/pull/503

diff --git a/stdnum/upi.py b/stdnum/upi.py
new file mode 100644
index 0000000..f333a6f
--- /dev/null
+++ b/stdnum/upi.py
@@ -0,0 +1,84 @@
+# upi.py - functions for handling Unique Product Identifiers (UPIs)
+#
+# Copyright (C) 2026 Jon Arnfred
+#
+# 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, see <https://www.gnu.org/licenses/>.
+
+"""UPI (ISO 4914 Unique Product Identifier).
+
+The Unique Product Identifier (UPI) is a 12-character alphanumeric code used
+to identify over-the-counter (OTC) derivative products. It consists of the
+two-character prefix ``QZ``, nine characters that identify the product, and
+one check character. Vowels and the letter Y are not used. The check
+character uses the ISO 7064 Mod 31, 30 algorithm.
+
+More information:
+
+* https://www.iso.org/standard/80506.html
+* https://www.anna-dsb.com/upi-/
+* https://zakon.rada.gov.ua/laws/show/z1549-21
+
+>>> validate('QZK12RNSP6P6')
+'QZK12RNSP6P6'
+>>> validate('QZK12RNSP6P7')
+Traceback (most recent call last):
+    ...
+InvalidChecksum: ...
+>>> calc_check_digit('QZK12RNSP6P')
+'6'
+"""
+
+from __future__ import annotations
+
+from stdnum.exceptions import *
+from stdnum.iso7064 import mod_37_36
+from stdnum.util import clean
+
+
+# The UPI alphabet excludes vowels and the letter Y. Using this alphabet with
+# the generic Mod x+1, x implementation gives the ISO 7064 Mod 31, 30 method.
+_alphabet = '0123456789BCDFGHJKLMNPQRSTVWXZ'
+
+
+def compact(number: str) -> str:
+    """Convert the UPI to its minimal representation. This strips spaces and
+    removes surrounding whitespace."""
+    return clean(number, ' ').strip().upper()
+
+
+def calc_check_digit(number: str) -> str:
+    """Calculate the check character for the number."""
+    return mod_37_36.calc_check_digit(compact(number), alphabet=_alphabet)
+
+
+def validate(number: str) -> str:
+    """Check if the number provided is a valid UPI. This checks the length,
+    format, prefix and check character."""
+    number = compact(number)
+    if not all(x in _alphabet for x in number):
+        raise InvalidFormat()
+    if len(number) != 12:
+        raise InvalidLength()
+    if not number.startswith('QZ'):
+        raise InvalidComponent()
+    mod_37_36.validate(number, alphabet=_alphabet)
+    return number
+
+
+def is_valid(number: str) -> bool:
+    """Check if the number provided is a valid UPI."""
+    try:
+        return bool(validate(number))
+    except ValidationError:
+        return False
diff --git a/tests/test_upi.doctest b/tests/test_upi.doctest
new file mode 100644
index 0000000..1d170e4
--- /dev/null
+++ b/tests/test_upi.doctest
@@ -0,0 +1,83 @@
+test_upi.doctest - more detailed doctests for the stdnum.upi module
+
+Copyright (C) 2026 Jon Arnfred
+
+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, see <https://www.gnu.org/licenses/>.
+
+
+This file contains more detailed doctests for the stdnum.upi module.
+
+>>> from stdnum import upi
+>>> from stdnum.exceptions import *
+
+
+Spaces and case are normalised by compact().
+
+>>> upi.compact(' qzk1 2rnsp6p6 ')
+'QZK12RNSP6P6'
+>>> upi.validate('qzk12rnsp6p6')
+'QZK12RNSP6P6'
+
+
+The prefix must be QZ and only digits and consonants other than Y are
+permitted.
+
+>>> upi.validate('XZK12RNSP6P6')
+Traceback (most recent call last):
+    ...
+InvalidComponent: ...
+>>> upi.validate('QZA12RNSP6P6')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+>>> upi.validate('QZY12RNSP6P6')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+>>> upi.validate('QZK12RNSP6P')
+Traceback (most recent call last):
+    ...
+InvalidLength: ...
+
+
+The final character is an ISO 7064 Mod 31, 30 check character.
+
+>>> upi.calc_check_digit('QZK12RNSP6P')
+'6'
+>>> upi.validate('QZK12RNSP6P7')
+Traceback (most recent call last):
+    ...
+InvalidChecksum: ...
+
+
+These examples have been published by the Derivatives Service Bureau, CME
+Group and in ISO 4914 supporting material.
+
+* 
https://www.anna-dsb.com/download/dsb-2770-isin-eq-sw-total-return-swap-basket-template-definition/
+* 
https://www.anna-dsb.com/download/dsb-2771-upi-eq-sw-total-return-swap-single-name-template-definition/
+* https://www.cmegroup.com/notices/reference-data-api/2024/02/20240222.html
+* https://zakon.rada.gov.ua/laws/show/z1549-21
+
+>>> numbers = '''
+...
+... QZDXL66WTF3C
+... QZK12RNSP6P6
+... QZNX2JD91QCG
+... QZVLFS6FH9VZ
+...
+... '''
+>>> [x for x in numbers.splitlines() if x and not upi.is_valid(x)]
+[]
+>>> upi.is_valid('QZK12RNSP6P7')
+False

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

Summary of changes:
 stdnum/upi.py          | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_upi.doctest | 83 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 167 insertions(+)
 create mode 100644 stdnum/upi.py
 create mode 100644 tests/test_upi.doctest


hooks/post-receive
-- 
python-stdnum