diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b94d63..be846ed 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,11 +5,22 @@ Changelog Versions follow `CalVer `_ with the scheme ``YY.0M.Micro``. -`2024.05.4`_ - tbd +`2024.05.4`_ - 2024/05/25 ------------------------- +Added +~~~~~ +* The ``IBAN`` and ``BBAN`` classes now have an additional property ``currency_code`` for countries + like Seychelles, Guatemala or Mauritius. + Fixed ~~~~~ -* Also allow the BIC lookup for non-primary banks. +* Also allow the BIC lookup for non-primary banks. For countries like Switzerland the lookup did + fail for banks which did not have the primary-flag set, even though an appropriate mapping was + available. +* ``IBAN.random()`` now also works for countries which have a currency code included in their BBAN + e.g. Mauritius or Seychelles. +* ``IBAN.random()`` now also works for aspirational countries, where no information of the BBAN + structure is available, e.g. Comoros. `2024.05.3`_ - 2024/05/10 ------------------------- diff --git a/schwifty/bban.py b/schwifty/bban.py index 8139859..5deb93d 100644 --- a/schwifty/bban.py +++ b/schwifty/bban.py @@ -193,6 +193,9 @@ def random( if (banks := banks_by_country.get(country_code)) is not None and use_registry: bank = random.choice(banks) + if "positions" not in spec: + return cls(country_code, rstr.xeger(spec["regex"]).upper()) + ranges = _get_position_ranges(spec) for _ in range(100): bban = rstr.xeger(spec["regex"]).upper() @@ -201,7 +204,9 @@ def random( if (value := values.get(key)) is not None: components[key] = value else: - components[key] = bank.get(key) or range_.cut(bban) + components[key] = bank.get(key) or spec.get( + f"default_{key.value}", range_.cut(bban) + ) bank_code = components[Component.BANK_CODE] bank_code_length = ranges[Component.BANK_CODE].length @@ -306,6 +311,14 @@ def account_holder_id(self) -> str: """ return self._get_component(Component.ACCOUNT_HOLDER_ID) + @property + def currency_code(self) -> str: + """str: The account's currency code. + + This value is only available for Mauretania, Seychelles and Guatemala. + """ + return self._get_component(Component.CURRENCY_CODE) + @property def bank(self) -> dict | None: """dict | None: The information of bank related to this BBANs bank code.""" diff --git a/schwifty/domain.py b/schwifty/domain.py index 292a31e..d9c6aa3 100644 --- a/schwifty/domain.py +++ b/schwifty/domain.py @@ -6,6 +6,7 @@ class Component(str, enum.Enum): ACCOUNT_TYPE = "account_type" ACCOUNT_CODE = "account_code" ACCOUNT_HOLDER_ID = "account_holder_id" + CURRENCY_CODE = "currency_code" BANK_CODE = "bank_code" BRANCH_CODE = "branch_code" NATIONAL_CHECKSUM_DIGITS = "national_checksum_digits" diff --git a/schwifty/iban.py b/schwifty/iban.py index a8d62f9..cfc8b8c 100644 --- a/schwifty/iban.py +++ b/schwifty/iban.py @@ -372,6 +372,14 @@ def account_holder_id(self) -> str: """ return self.bban.account_holder_id + @property + def currency_code(self) -> str: + """str: The account's currency code. + + This value is only available for Mauretania, Seychelles and Guatemala. + """ + return self.bban.currency_code + @property def bank(self) -> dict | None: """dict or None: The information of the bank related to the bank code as part of the BBAN""" @@ -442,15 +450,19 @@ def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) - def add_bban_regex(country: str, spec: dict) -> dict: - bban_spec = spec["bban_spec"] + if "regex" not in spec: + spec["regex"] = re.compile(convert_bban_spec_to_regex(spec["bban_spec"])) + return spec + + +def convert_bban_spec_to_regex(spec: str) -> str: spec_re = rf"(\d+)(!)?([{''.join(_spec_to_re.keys())}])" def convert(match: re.Match) -> str: quantifier = ("{{{}}}" if match.group(2) else "{{1,{}}}").format(match.group(1)) return _spec_to_re[match.group(3)] + quantifier - spec["regex"] = re.compile(rf"^{re.sub(spec_re, convert, bban_spec)}$") - return spec + return rf"^{re.sub(spec_re, convert, spec)}$" registry.manipulate("iban", add_bban_regex) diff --git a/schwifty/iban_registry/overwrite.json b/schwifty/iban_registry/overwrite.json index 2fa1e94..53e4750 100644 --- a/schwifty/iban_registry/overwrite.json +++ b/schwifty/iban_registry/overwrite.json @@ -498,6 +498,22 @@ ] } }, + "GT": { + "positions": { + "currency_code": [ + 4, + 6 + ], + "account_type": [ + 6, + 8 + ], + "account_code": [ + 8, + 24 + ] + } + }, "GW": { "country": "GW", "bban_spec": "2!c19!n", @@ -754,8 +770,13 @@ "account_code": [ 8, 20 + ], + "currency_code": [ + 23, + 26 ] - } + }, + "default_currency_code": "MUR" }, "MZ": { "country": "MZ", @@ -922,11 +943,12 @@ 8, 24 ], - "account_type": [ + "currency_code": [ 24, 27 ] - } + }, + "default_currency_code": "SCR" }, "SE": { "positions": { diff --git a/tests/test_iban.py b/tests/test_iban.py index 850f109..336c2bb 100644 --- a/tests/test_iban.py +++ b/tests/test_iban.py @@ -5,6 +5,7 @@ from schwifty import IBAN from schwifty.exceptions import SchwiftyException +from schwifty.iban import convert_bban_spec_to_regex valid = [ @@ -365,6 +366,17 @@ def test_random_iban() -> None: assert isinstance(iban, IBAN) +def test_random_special_cases() -> None: + iban = IBAN.random(country_code="MU") + assert iban.endswith("000MUR") + + iban = IBAN.random(country_code="SC") + assert iban.endswith("SCR") + + iban = IBAN.random(country_code="KM") + assert iban.is_valid + + def test_pydantic_protocol() -> None: from pydantic import BaseModel from pydantic import ValidationError @@ -392,3 +404,18 @@ class Model(BaseModel): loaded = Model.model_validate_json(dumped) assert loaded == model + + +@pytest.mark.parametrize( + ("spec", "regex"), + [ + ("5!n", r"^\d{5}$"), + ("4!a", r"^[A-Z]{4}$"), + ("10!c", r"^[A-Za-z0-9]{10}$"), + ("5!e", r"^ {5}$"), + ("3n", r"^\d{1,3}$"), + ("5!n3!a", r"^\d{5}[A-Z]{3}$"), + ], +) +def test_convert_bban_spec_to_regex(spec: str, regex: str) -> None: + assert convert_bban_spec_to_regex(spec) == regex