Skip to content

Commit

Permalink
Fix random calculation for various edge cases
Browse files Browse the repository at this point in the history
  • Loading branch information
mdomke committed May 25, 2024
1 parent 335a397 commit c467335
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 9 deletions.
15 changes: 13 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@ Changelog

Versions follow `CalVer <http://www.calver.org/>`_ 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
-------------------------
Expand Down
15 changes: 14 additions & 1 deletion schwifty/bban.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
1 change: 1 addition & 0 deletions schwifty/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
18 changes: 15 additions & 3 deletions schwifty/iban.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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)
28 changes: 25 additions & 3 deletions schwifty/iban_registry/overwrite.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -754,8 +770,13 @@
"account_code": [
8,
20
],
"currency_code": [
23,
26
]
}
},
"default_currency_code": "MUR"
},
"MZ": {
"country": "MZ",
Expand Down Expand Up @@ -922,11 +943,12 @@
8,
24
],
"account_type": [
"currency_code": [
24,
27
]
}
},
"default_currency_code": "SCR"
},
"SE": {
"positions": {
Expand Down
27 changes: 27 additions & 0 deletions tests/test_iban.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from schwifty import IBAN
from schwifty.exceptions import SchwiftyException
from schwifty.iban import convert_bban_spec_to_regex


valid = [
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit c467335

Please sign in to comment.