Skip to content

Commit

Permalink
Improve Pydantic support for BIC objects
Browse files Browse the repository at this point in the history
  • Loading branch information
mdomke committed May 7, 2024
1 parent aa251c2 commit 99a1dcc
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 14 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Versions follow `CalVer <http://www.calver.org/>`_ with the scheme ``YY.0M.Micro
-------------------------
Fixed
~~~~~
* Loading JSON data into a Pydantic model with an ``IBAN``-field (``Model.model_validate_json()``)
was previously broken and has been fixed now.
* Loading JSON data into a Pydantic model with an ``IBAN`` or ``BIC``-field
(``Model.model_validate_json()``) was previously broken and has been fixed now.

Added
~~~~~
Expand All @@ -19,6 +19,7 @@ Added
Changed
~~~~~~~
* Updated bank registries.
* Remove the dependency to ``iso3166`` since its functionallity is already covered by ``pycountry``


`2024.04.0`_ - 2024/04/18
Expand Down
57 changes: 46 additions & 11 deletions schwifty/bic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

if TYPE_CHECKING:
from pydantic import GetCoreSchemaHandler
from pydantic import GetJsonSchemaHandler
from pydantic import ValidatorFunctionWrapHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema

_bic_iso9362_re = re.compile(r"[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?")
Expand Down Expand Up @@ -285,6 +288,49 @@ def _validate_country_code(self) -> None:
if self.country is None:
raise exceptions.InvalidCountryCode(f"Invalid country code '{self.country_code}'")

@classmethod
def __get_pydantic_core_schema__(
cls, source: type[Any], handler: GetCoreSchemaHandler
) -> CoreSchema:
from pydantic_core import core_schema

return core_schema.no_info_wrap_validator_function(
cls._pydantic_validate,
core_schema.union_schema(
[
core_schema.is_instance_schema(BIC),
core_schema.no_info_plain_validator_function(BIC),
core_schema.str_schema(
pattern=r"[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}(:?[A-Z0-9]{3})?",
min_length=8,
max_length=11,
),
]
),
serialization=core_schema.to_string_ser_schema(
when_used="json-unless-none",
),
)

@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
json_schema = handler(core_schema)
json_schema = handler.resolve_ref_schema(json_schema)
json_schema["title"] = "BIC"
return json_schema

@classmethod
def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
from pydantic_core import PydanticCustomError

try:
bic = cls(value)
except exceptions.SchwiftyException as err:
raise PydanticCustomError("bic_format", str(err)) from err
return handler(bic)

@property
def is_valid(self) -> bool:
"""bool: Indicate if this is a valid BIC.
Expand Down Expand Up @@ -446,17 +492,6 @@ def branch_code(self) -> str:
"""str: The branch-code part of the BIC (if available)"""
return self._get_slice(start=8, end=11)

@classmethod
def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
from pydantic_core import core_schema

return core_schema.union_schema(
[
core_schema.is_instance_schema(BIC),
core_schema.no_info_plain_validator_function(BIC),
]
)


registry.build_index("bank", "bic", key="bic", accumulate=True)
registry.build_index(
Expand Down
14 changes: 13 additions & 1 deletion tests/test_bic.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,18 @@ class Model(BaseModel):

model = Model(bic="GENODEM1GLS") # type: ignore[arg-type]
assert isinstance(model.bic, BIC)
assert model.model_dump() == {"bic": model.bic}

with pytest.raises(ValidationError):
with pytest.raises(ValidationError) as err:
Model(bic="GENODXM1GLS") # type: ignore[arg-type]
assert len(err.value.errors()) == 1
error = err.value.errors()[0]
assert error["type"] == "bic_format"
assert error["msg"] == "Invalid country code 'DX'"
assert error["input"] == "GENODXM1GLS"

dumped = model.model_dump_json()
assert dumped == '{"bic":"GENODEM1GLS"}'

loaded = Model.model_validate_json(dumped)
assert loaded == model

0 comments on commit 99a1dcc

Please sign in to comment.