lists.arthurdejong.org
RSS feed

python-stdnum commit: r51 - in python-stdnum: . stdnum tests

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

python-stdnum commit: r51 - in python-stdnum: . stdnum tests



Author: arthur
Date: Sun Jan 16 22:48:12 2011
New Revision: 51
URL: http://arthurdejong.org/viewvc/python-stdnum?view=rev&revision=51

Log:
add an IBAN (International Bank Account Number) module

Added:
   python-stdnum/getiban.py   (contents, props changed)
   python-stdnum/stdnum/iban.dat
   python-stdnum/stdnum/iban.py
   python-stdnum/tests/test_iban.doctest
Modified:
   python-stdnum/README

Modified: python-stdnum/README
==============================================================================
--- python-stdnum/README        Sun Jan 16 22:23:43 2011        (r50)
+++ python-stdnum/README        Sun Jan 16 22:48:12 2011        (r51)
@@ -17,6 +17,7 @@
 - IMEI (International Mobile Equipment Identity)
 - MEID (Mobile Equipment Identifier)
 - GRid (Global Release Identifier)
+- IBAN (International Bank Account Number)
 
 Furthermore a number of generic check digit algorithms are available:
 

Added: python-stdnum/getiban.py
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ python-stdnum/getiban.py    Sun Jan 16 22:48:12 2011        (r51)
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+
+# getiban.py - script to donwload and parse data from the IBAN registry
+#
+# Copyright (C) 2011 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 data from SWIFT (the Society for Worldwide Interbank
+Financial Telecommunication which is the official IBAN registrar) to get
+the data needed to correctly parse and validate IBANs."""
+
+import urllib
+import re
+
+
+# The place where the current version of IBAN_Registry.txt can be downloaded.
+download_url = 'http://www.swift.com/dsp/resources/documents/IBAN_Registry.txt'
+
+def splitlines(f):
+    """Read lines from the TAB-delimited IBAN_Registry.txt file and return
+    a dictionary per read line. We clean up the values a bit because it
+    contains some junk."""
+    stripit = ' \t\n\r;:\'"'
+    firstline = [ x.strip(stripit) for x in f.readline().lower().split('\t') ]
+    for line in f:
+        yield dict(zip(firstline, [ x.strip(stripit) for x in line.split('\t') 
]))
+
+def get_country_codes(line):
+    """Return the list of country codes this line has."""
+    # simplest case first
+    if len(line['country code as defined in iso 3166']) == 2:
+        return [ line['country code as defined in iso 3166'] ]
+    # fall back to parsing the IBAN structure
+    return [ x.strip()[:2] for x in line['iban structure'].split(',') ]
+
+def parse(f):
+    """Parse the specified file."""
+    print '# generated from IBAN_Registry.txt, downloaded from'
+    print '# %s' % download_url
+    for line in splitlines(f):
+        for cc in get_country_codes(line):
+            # print country line
+            print '%s country="%s" bban="%s"' % ( cc, line['name of country'], 
line['bban structure'])
+            # TODO: some countries have a fixed check digit value
+            # TODO: some countries have extra check digits
+            # TODO: use "Bank identifier position within the BBAN" field
+            #       to add labels to the ranges (Bank identifier and Branch 
Identifier)
+
+if __name__ == '__main__':
+    #f = open('IBAN_Registry.txt', 'r')
+    f = urllib.urlopen(download_url)
+    parse(f)

Added: python-stdnum/stdnum/iban.dat
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ python-stdnum/stdnum/iban.dat       Sun Jan 16 22:48:12 2011        (r51)
@@ -0,0 +1,55 @@
+# generated from IBAN_Registry.txt, downloaded from
+# http://www.swift.com/dsp/resources/documents/IBAN_Registry.txt
+AL country="Albania" bban="8!n16!c"
+AD country="Andorra" bban="4!n4!n12!c"
+AT country="Austria" bban="5!n11!n"
+BE country="Belgium" bban="3!n7!n2!n"
+BA country="Bosnia and Herzegovina" bban="3!n3!n8!n2!n"
+BG country="Bulgaria" bban="4!a4!n2!n8!c"
+HR country="Croatia" bban="7!n10!n"
+CY country="Cyprus" bban="3!n5!n16!c"
+CZ country="Czech Republic" bban="4!n6!n10!n"
+DK country="Denmark" bban="4!n9!n1!n"
+FO country="Denmark" bban="4!n9!n1!n"
+GL country="Denmark" bban="4!n9!n1!n"
+EE country="Estonia" bban="2!n2!n11!n1!n"
+FI country="Finland" bban="6!n7!n1!n"
+FR country="France" bban="5!n5!n11!c2!n"
+GE country="Georgia" bban="2!a16!n"
+DE country="Germany" bban="8!n10!n"
+GI country="Gibraltar" bban="4!a15!c"
+GR country="Greece" bban="3!n4!n16!c"
+HU country="Hungary" bban="3!n4!n1!n15!n1!n"
+IS country="Iceland" bban="4!n2!n6!n10!n"
+IE country="Ireland" bban="4!a6!n8!n"
+IL country="Israel" bban="3!n3!n13!n"
+IT country="Italy" bban="1!a5!n5!n12!c"
+KW country="Kuwait" bban="4!a22!c"
+LV country="Latvia" bban="4!a13!c"
+LB country="Lebanon" bban="4!n20!c"
+LI country="Liechtenstein (Principality of)" bban="5!n12!c"
+LT country="Lithuania" bban="5!n11!n"
+LU country="Luxembourg" bban="3!n13!c"
+MK country="Macedonia, Former Yugoslav Republic of" bban="3!n10!c2!n"
+MT country="Malta" bban="4!a5!n18!c"
+MR country="Mauritania" bban="5!n5!n11!n2!n"
+MU country="Mauritius" bban="4!a2!n2!n12!n3!n3!a"
+MC country="Monaco" bban="5!n5!n11!c2!n"
+ME country="Montenegro" bban="3!n13!n2!n"
+NL country="The Netherlands" bban="4!a10!n"
+NO country="Norway" bban="4!n6!n1!n"
+PL country="Poland" bban="8!n16!n"
+PT country="Portugal" bban="4!n4!n11!n2!n"
+RO country="Romania" bban="4!a16!c"
+SM country="San Marino" bban="1!a5!n5!n12!c"
+SA country="Saudi Arabia" bban="2!n18!c"
+RS country="Serbia" bban="3!n13!n2!n"
+SK country="Slovak Republic" bban="4!n6!n10!n"
+SI country="Slovenia" bban="5!n8!n2!n"
+ES country="Spain" bban="4!n4!n1!n1!n10!n"
+SE country="Sweden" bban="3!n16!n1!n"
+CH country="Switzerland" bban="5!n12!c"
+TN country="Tunisia" bban="2!n3!n13!n2!n"
+TR country="Turkey" bban="5!n1!c16!c"
+AE country="United Arab Emirates" bban="3!n16!n"
+GB country="United Kingdom" bban="4!a6!n8!n"

Added: python-stdnum/stdnum/iban.py
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ python-stdnum/stdnum/iban.py        Sun Jan 16 22:48:12 2011        (r51)
@@ -0,0 +1,91 @@
+# iban.py - functions for handling International Bank Account Numbers (IBANs)
+#
+# Copyright (C) 2011 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
+
+"""Module for handling IBANs (International Bank Account Numbers)
+numbers, used to identify bank accounts across national borders.
+
+
+>>> is_valid('GR16 0110 1050 0000 1054 7023 795')
+True
+>>> is_valid('BE31435411161155')
+True
+>>> compact('GR16 0110 1050 0000 1054 7023 795')
+'GR1601101050000010547023795'
+>>> format('GR1601101050000010547023795')
+'GR16 0110 1050 0000 1054 7023 795'
+"""
+
+from stdnum import numdb
+from stdnum.iso7064 import mod_97_10
+import re
+
+
+# our open copy of the IBAN database
+_ibandb = numdb.get('iban')
+
+def compact(number):
+    """Convert the iban number to the minimal representation. This strips the
+    number of any valid separators and removes surrounding whitespace."""
+    return number.replace(' ','').replace('-','').strip().upper()
+
+_alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+
+def _convert(number):
+    """Prepare the number to it's base10 representation (also moving the
+    check digits to the end) so it can be checked with the ISO 7064
+    Mod 97, 10 algorithm."""
+    # TODO: find out whether this should be in the mod_97_10 module
+    return ''.join(str(_alphabet.index(x)) for x in number[4:] + number[:4])
+
+# regular expression to check IBAN structure
+_struct_re = re.compile('([1-9][0-9]*)!([nac])')
+
+def _matches_structure(number, structure):
+    """Check the supplied number against the supplied structure."""
+    start = 0
+    for length, code in _struct_re.findall(structure):
+        length = int(length)
+        if code == 'n' and not number[start:start + length].isdigit():
+            return False
+        elif code == 'a' and not number[start:start + length].isalpha():
+            return False
+        elif code == 'c' and not number[start:start + length].isalnum():
+            return False # should not happen due to checksum check
+        start += length
+    # the whole number should be parsed now
+    return start == len(number)
+
+def is_valid(number):
+    """Checks to see if the number provided is a valid IBAN."""
+    try:
+        number = compact(number)
+    except:
+        return False
+    # ensure that checksum is valid
+    if not mod_97_10.is_valid(_convert(number)):
+        return False
+    # look up the number
+    info = _ibandb.info(number)
+    # check if the number has the correct structure
+    return _matches_structure(number[4:], info[0][1].get('bban', ''))
+
+def format(number, separator=' '):
+    """Reformat the passed number to the space-separated format."""
+    number = compact(number)
+    return separator.join(number[i:i+4] for i in range(0,len(number),4))

Added: python-stdnum/tests/test_iban.doctest
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ python-stdnum/tests/test_iban.doctest       Sun Jan 16 22:48:12 2011        
(r51)
@@ -0,0 +1,198 @@
+test_iban.doctest - more detailed doctests for the stdnum.iban module
+
+Copyright (C) 2011 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.iban module. It
+tries to test more corner cases and detailed functionality that is not
+really useful as module documentation.
+
+>>> from stdnum import iban
+
+
+These should all be valid numbers and are from the IBAN REGISTRY as sample
+numbers:
+
+>>> numbers = '''
+... AL47212110090000000235698741
+... AD1200012030200359100100
+... AT611904300234573201
+... BE68539007547034
+... BA391290079401028494
+... BG80BNBG96611020345678
+... HR1210010051863000160
+... CY17002001280000001200527600
+... CZ6508000000192000145399
+... CZ9455000000001011038930
+... DK5000400440116243
+... FO6264600001631634
+... GL8964710001000206
+... EE382200221020145685
+... FI2112345600000785
+... FI5542345670000081
+... FR1420041010050500013M02606
+... DE89370400440532013000
+... GE29NB0000000101904917
+... GI75NWBK000000007099453
+... GR1601101250000000012300695
+... HU42117730161111101800000000
+... IS140159260076545510730339
+... IE29AIBK93115212345678
+... IL620108000000099999999
+... IT60X0542811101000000123456
+... LV80BANK0000435195001
+... LB62099900000001001901229114
+... LI21088100002324013AA
+... LT121000011101001000
+... LU280019400644750000
+... MK07250120000058984
+... MT84MALT011000012345MTLCAST001S
+... MR1300020001010000123456753
+... MU17 BOMM0101101030300200000MUR
+... MC1112739000700011111000h79
+... ME25505000012345678951
+... NL91ABNA0417164300
+... NO9386011117947
+... PL61109010140000071219812874
+... PT50000201231234567890154
+... RO49AAAA1B31007593840000
+... SM86U0322509800000000270100
+... SA0380000000608010167519
+... RS35260005601001611379
+... SK3112000000198742637541
+... SI56191000000123438
+... ES9121000418450200051332
+... SE4550000000058398257466
+... CH9300762011623852957
+... TN5910006035183598478831
+... TR330006100519786457841326
+... GB29NWBK60161331926819
+... AL47 2121 1009 0000 0002 3569 8741
+... AD12 0001 2030 2003 5910 0100
+... AT61 1904 3002 3457 3201
+... BE68 5390 0754 7034
+... BA39 1290 0794 0102 8494
+... BG80 BNBG 9661 1020 3456 78
+... HR12 1001 0051 8630 0016 0
+... CY17 0020 0128 0000 0012 0052 7600
+... CZ65 0800 0000 1920 0014 5399
+... CZ94 5500 0000 0010 1103 8930
+... DK50 0040 0440 1162 43
+... FO62 6460 0001 6316 34
+... GL89 6471 0001 0002 06
+... EE38 2200 2210 2014 5685
+... FI21 1234 5600 0007 85
+... FR14 2004 1010 0505 0001 3M02 606
+... DE89 3704 0044 0532 0130 00
+... GE29 NB00 0000 0101 9049 17
+... GI75 NWBK 0000 0000 7099 453
+... GR16 0110 1250 0000 0001 2300 695
+... HU42 1177 3016 1111 1018 0000 0000
+... IS14 0159 2600 7654 5510 7303 39
+... IE29 AIBK 9311 5212 3456 78
+... IL62 0108 0000 0009 9999 999
+... IT60 X054 2811 1010 0000 0123 456
+... LV80 BANK 0000 4351 9500 1
+... LB62 0999 0000 0001 0019 0122 9114
+... LI21 0881 0000 2324 013A A
+... LT12 1000 0111 0100 1000
+... LU28 0019 4006 4475 0000
+... MK072 5012 0000 0589 84
+... MT84 MALT 0110 0001 2345 MTLC AST0 01S
+... MR13 0002 0001 0100 0012 3456 753
+... MU17 BOMM 0101 1010 3030 0200 000M UR
+... MC11 1273 9000 7000 1111 1000 h79
+... ME25 5050 0001 2345 6789 51
+... NL91 ABNA 0417 1643 00
+... NO93 8601 1117 947
+... PL61 1090 1014 0000 0712 1981 2874
+... PT50 0002 0123 1234 5678 9015 4
+... RO49 AAAA 1B31 0075 9384 0000
+... SM86 U032 2509 8000 0000 0270 100
+... SA03 8000 0000 6080 1016 7519
+... RS35 2600 0560 1001 6113 79
+... SK31 1200 0000 1987 4263 7541
+... SI56 1910 0000 0123 438
+... ES91 2100 0418 4502 0005 1332
+... SE45 5000 0000 0583 9825 7466
+... CH93 0076 2011 6238 5295 7
+... TN59 1000 6035 1835 9847 8831
+... TR33 0006 1005 1978 6457 8413 26
+... GB29 NWBK 6016 1331 9268 19
+... '''
+>>> [ x for x in numbers.splitlines() if x and not iban.is_valid(x) ]
+[]
+
+
+These all have broken checksums or are mangled:
+
+>>> numbers = '''
+... AL48212110090000000235698741
+... AD1210012030200359100100
+... AT611804300234573201
+... BE68530907547034
+... BA391290709401028494
+... BG80BNBG99611020345678
+... HR1210010052863000160
+... CY17002001281000001200527600
+... CZ6508000000129000145399
+... CZ9455000000000101038930
+... DK5000400440116342
+... '''
+>>> [ x for x in numbers.splitlines() if x and iban.is_valid(x) ]
+[]
+
+
+These are the wrong length but otherwise have valid checksums:
+
+>>> numbers = '''
+... AD48 0001 2030 2003 5910 01
+... AT93 1904 3002 3457 3201 99
+... BE36 5390 0754 7034 1234
+... BA30 1290 0794 0102 84
+... '''
+>>> [ x for x in numbers.splitlines() if x and iban.is_valid(x) ]
+[]
+
+
+These are mostly valid except that they have alphabetic characters where
+they are not allowed or numbers where alphabetic characters are expected
+or have an unknown country code:
+
+>>> numbers = '''
+... BG93 BNBG 9661 1A20 3456 78
+... GI22 NW3K 0000 0000 7099 453
+... NL56 3003 0A17 1643 00
+... QQ93 1234 5678
+... '''
+>>> [ x for x in numbers.splitlines() if x and iban.is_valid(x) ]
+[]
+
+
+The is_valid() method should be fairly robust against invalid junk passed:
+
+>>> iban.is_valid(None)
+False
+>>> iban.is_valid('')
+False
+>>> iban.is_valid(0)
+False
+>>> iban.is_valid(object())
+False
+>>> iban.is_valid('3abc6800-0041X1-20')
+False
--
To unsubscribe send an email to
python-stdnum-commits-unsubscribe@lists.arthurdejong.org or see
http://lists.arthurdejong.org/python-stdnum-commits