Source code for schwifty.iban

# encoding: utf-8

from functools import partial
import re
import string

from schwifty.bic import BIC
from schwifty.common import Base
from schwifty import registry


_spec_to_re = {
    'n': r'\d',
    'a': r'[A-Z]',
    'c': r'[A-Za-z0-9]',
    'e': r' '
}

_alphabet = string.digits + string.ascii_uppercase


def _get_iban_spec(country_code):
    try:
        return registry.get('iban')[country_code]
    except KeyError:
        raise ValueError("Unknown country-code '{}'".format(country_code))


def numerify(string):
    return int(''.join(str(_alphabet.index(c)) for c in string))


def code_length(spec, code_type):
    start, end = spec['positions'][code_type]
    return end - start


[docs]class IBAN(Base): """The IBAN object. Examples: You create a new IBAN object by supplying an IBAN code in text form. The IBAN is validated behind the scenes and you can then access all relevant components as properties:: >>> iban = IBAN('DE89 3704 0044 0532 0130 00') >>> iban.account_number '0532013000' >>> iban.bank_code '37040044' >>> iban.country_code 'DE' >>> iban.check_digits '89' Args: iban (str): The IBAN code. allow_invalid (bool): If set to `True` IBAN validation is skipped on instantiation. """ def __init__(self, iban, allow_invalid=False): super(IBAN, self).__init__(iban) if self.checksum_digits == '??': self._code = self.country_code + self._calc_checksum_digits() + self.bban if not allow_invalid: self.validate() def _calc_checksum_digits(self): return '{:02d}'.format(98 - (numerify(self.bban + self.country_code) * 100) % 97) @classmethod
[docs] def generate(cls, country_code, bank_code, account_code): """Generate an IBAN from it's components. If the bank-code and/or account-number have less digits than required by their country specific representation, the respective component is padded with zeros. Examples: To generate an IBAN do the following:: >>> bank_code = '37040044' >>> account_code = '532013000' >>> iban = IBAN.generate('DE', bank_code, account_code) >>> iban.formatted 'DE89 3704 0044 0532 0130 00' Args: country_code (str): The ISO 3166 alpha-2 country code. bank_code (str): The country specific bank-code. account_code (str): The customer specific account-code. """ spec = _get_iban_spec(country_code) bank_code_length = code_length(spec, 'bank_code') branch_code_length = code_length(spec, 'branch_code') bank_and_branch_code_length = bank_code_length + branch_code_length account_code_length = code_length(spec, 'account_code') if len(bank_code) > bank_and_branch_code_length: raise ValueError( "Bank code exceeds maximum size {}".format(bank_and_branch_code_length)) if len(account_code) > account_code_length: raise ValueError( "Account code exceeds maximum size {}".format(account_code_length)) bank_code = bank_code.rjust(bank_and_branch_code_length, '0') account_code = account_code.rjust(account_code_length, '0') iban = country_code + '??' + bank_code + account_code return cls(iban)
def validate(self): self._validate_characters() self._validate_length() self._validate_format() self._validate_checksum() return True def _validate_characters(self): if not re.match(r'[A-Z]{2}\d{2}[A-Z]*', self.compact): raise ValueError("Invalid characters in IBAN {}".format(self.compact)) def _validate_checksum(self): if self.numeric % 97 != 1: raise ValueError("Invalid checksum digits") def _validate_length(self): if self.spec['iban_length'] != self.length: raise ValueError("Invalid IBAN length") def _validate_format(self): if not self.spec['regex'].match(self.bban): raise ValueError("Invalid BBAN structure: '{}' doesn't match '{}''".format( self.bban, self.spec['bban_spec'])) @property def numeric(self): """int: A numeric represenation of the IBAN.""" return numerify(self.bban + self.compact[:4]) @property def formatted(self): """str: The IBAN formatted in blocks of 4 digits.""" return ' '.join(self.compact[i:i + 4] for i in range(0, len(self.compact), 4)) @property def spec(self): """dict: The country specific IBAN specification.""" return _get_iban_spec(self.country_code) @property def bic(self): """BIC: The BIC associated to the IBANĀ“s bank-code.""" return BIC.from_bank_code(self.country_code, self.bank_code) def _get_code(self, code_type): start, end = self.spec['positions'][code_type] return self.bban[start:end] bban = property(partial(Base._get_component, start=4), doc="str: The BBAN part of the IBAN.") country_code = property(partial(Base._get_component, start=0, end=2), doc="str: ISO 3166 alpha-2 country code.") checksum_digits = property(partial(Base._get_component, start=2, end=4), doc="str: Two digit checksum of the IBAN.") bank_code = property(partial(_get_code, code_type='bank_code'), doc="str: The country specific bank-code.") branch_code = property(partial(_get_code, code_type='branch_code'), doc="str or None: The branch-code of the bank if available.") account_code = property(partial(_get_code, code_type='account_code'), doc="str: The customer specific account-code")
def add_bban_regex(country, spec): bban_spec = spec['bban_spec'] spec_re = r'(\d+)(!)?([{}])'.format(''.join(_spec_to_re.keys())) def convert(match): quantifier = ('{%s}' if match.group(2) else '{1,%s}') % match.group(1) return _spec_to_re[match.group(3)] + quantifier spec['regex'] = re.compile('^{}$'.format(re.sub(spec_re, convert, bban_spec))) return spec registry.manipulate('iban', add_bban_regex)