-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add currency code ISO 4217 and its subset that includes only currencies * added ISO 4217 * added tests * ♻️ Update currency code parsing --------- Co-authored-by: 07pepa <“no@sharebcs.spam”> Co-authored-by: Yasser Tahiri <yasserth19@gmail.com>
- Loading branch information
1 parent
e727b1f
commit 10386a9
Showing
3 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
""" | ||
Currency definitions that are based on the [ISO4217](https://en.wikipedia.org/wiki/ISO_4217). | ||
""" | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler | ||
from pydantic_core import PydanticCustomError, core_schema | ||
|
||
try: | ||
import pycountry | ||
except ModuleNotFoundError: # pragma: no cover | ||
raise RuntimeError( | ||
'The `currency_code` module requires "pycountry" to be installed. You can install it with "pip install ' | ||
'pycountry".' | ||
) | ||
|
||
# List of codes that should not be usually used within regular transactions | ||
_CODES_FOR_BONDS_METAL_TESTING = { | ||
'XTS', # testing | ||
'XAU', # gold | ||
'XAG', # silver | ||
'XPD', # palladium | ||
'XPT', # platinum | ||
'XBA', # Bond Markets Unit European Composite Unit (EURCO) | ||
'XBB', # Bond Markets Unit European Monetary Unit (E.M.U.-6) | ||
'XBC', # Bond Markets Unit European Unit of Account 9 (E.U.A.-9) | ||
'XBD', # Bond Markets Unit European Unit of Account 17 (E.U.A.-17) | ||
'XXX', # no currency | ||
'XDR', # SDR (Special Drawing Right) | ||
} | ||
|
||
|
||
class ISO4217(str): | ||
"""ISO4217 parses Currency in the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) format. | ||
```py | ||
from pydantic import BaseModel | ||
from pydantic_extra_types.currency_code import ISO4217 | ||
class Currency(BaseModel): | ||
alpha_3: ISO4217 | ||
currency = Currency(alpha_3='AED') | ||
print(currency) | ||
# > alpha_3='AED' | ||
``` | ||
""" | ||
|
||
allowed_countries_list = [country.alpha_3 for country in pycountry.currencies] | ||
allowed_currencies = set(allowed_countries_list) | ||
|
||
@classmethod | ||
def _validate(cls, currency_code: str, _: core_schema.ValidationInfo) -> str: | ||
""" | ||
Validate a ISO 4217 language code from the provided str value. | ||
Args: | ||
currency_code: The str value to be validated. | ||
_: The Pydantic ValidationInfo. | ||
Returns: | ||
The validated ISO 4217 currency code. | ||
Raises: | ||
PydanticCustomError: If the ISO 4217 currency code is not valid. | ||
""" | ||
if currency_code not in cls.allowed_currencies: | ||
raise PydanticCustomError( | ||
'ISO4217', 'Invalid ISO 4217 currency code. See https://en.wikipedia.org/wiki/ISO_4217' | ||
) | ||
return currency_code | ||
|
||
@classmethod | ||
def __get_pydantic_core_schema__(cls, _: type[Any], __: GetCoreSchemaHandler) -> core_schema.CoreSchema: | ||
return core_schema.with_info_after_validator_function( | ||
cls._validate, | ||
core_schema.str_schema(min_length=3, max_length=3), | ||
) | ||
|
||
@classmethod | ||
def __get_pydantic_json_schema__( | ||
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler | ||
) -> dict[str, Any]: | ||
json_schema = handler(schema) | ||
json_schema.update({'enum': cls.allowed_countries_list}) | ||
return json_schema | ||
|
||
|
||
class Currency(str): | ||
"""Currency parses currency subset of the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) format. | ||
It excludes bonds testing codes and precious metals. | ||
```py | ||
from pydantic import BaseModel | ||
from pydantic_extra_types.currency_code import Currency | ||
class currency(BaseModel): | ||
alpha_3: Currency | ||
cur = currency(alpha_3='AED') | ||
print(cur) | ||
# > alpha_3='AED' | ||
``` | ||
""" | ||
|
||
allowed_countries_list = list( | ||
filter(lambda x: x not in _CODES_FOR_BONDS_METAL_TESTING, ISO4217.allowed_countries_list) | ||
) | ||
allowed_currencies = set(allowed_countries_list) | ||
|
||
@classmethod | ||
def _validate(cls, currency_symbol: str, _: core_schema.ValidationInfo) -> str: | ||
""" | ||
Validate a subset of the [ISO4217](https://en.wikipedia.org/wiki/ISO_4217) format. | ||
It excludes bonds testing codes and precious metals. | ||
Args: | ||
currency_symbol: The str value to be validated. | ||
_: The Pydantic ValidationInfo. | ||
Returns: | ||
The validated ISO 4217 currency code. | ||
Raises: | ||
PydanticCustomError: If the ISO 4217 currency code is not valid or is bond, precious metal or testing code. | ||
""" | ||
if currency_symbol not in cls.allowed_currencies: | ||
raise PydanticCustomError( | ||
'InvalidCurrency', | ||
'Invalid currency code.' | ||
' See https://en.wikipedia.org/wiki/ISO_4217. ' | ||
'Bonds, testing and precious metals codes are not allowed.', | ||
) | ||
return currency_symbol | ||
|
||
@classmethod | ||
def __get_pydantic_core_schema__(cls, _: type[Any], __: GetCoreSchemaHandler) -> core_schema.CoreSchema: | ||
""" | ||
Return a Pydantic CoreSchema with the currency subset of the | ||
[ISO4217](https://en.wikipedia.org/wiki/ISO_4217) format. | ||
It excludes bonds testing codes and precious metals. | ||
Args: | ||
_: The source type. | ||
__: The handler to get the CoreSchema. | ||
Returns: | ||
A Pydantic CoreSchema with the subset of the currency subset of the | ||
[ISO4217](https://en.wikipedia.org/wiki/ISO_4217) format. | ||
It excludes bonds testing codes and precious metals. | ||
""" | ||
return core_schema.with_info_after_validator_function( | ||
cls._validate, | ||
core_schema.str_schema(min_length=3, max_length=3), | ||
) | ||
|
||
@classmethod | ||
def __get_pydantic_json_schema__( | ||
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler | ||
) -> dict[str, Any]: | ||
""" | ||
Return a Pydantic JSON Schema with subset of the [ISO4217](https://en.wikipedia.org/wiki/ISO_4217) format. | ||
Excluding bonds testing codes and precious metals. | ||
Args: | ||
schema: The Pydantic CoreSchema. | ||
handler: The handler to get the JSON Schema. | ||
Returns: | ||
A Pydantic JSON Schema with the subset of the ISO4217 currency code validation. without bonds testing codes | ||
and precious metals. | ||
""" | ||
json_schema = handler(schema) | ||
json_schema.update({'enum': cls.allowed_countries_list}) | ||
return json_schema |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import re | ||
|
||
import pycountry | ||
import pytest | ||
from pydantic import BaseModel, ValidationError | ||
|
||
from pydantic_extra_types import currency_code | ||
|
||
|
||
class ISO4217CheckingModel(BaseModel): | ||
currency: currency_code.ISO4217 | ||
|
||
|
||
class CurrencyCheckingModel(BaseModel): | ||
currency: currency_code.Currency | ||
|
||
|
||
forbidden_currencies = sorted(currency_code._CODES_FOR_BONDS_METAL_TESTING) | ||
|
||
|
||
@pytest.mark.parametrize('currency', map(lambda code: code.alpha_3, pycountry.currencies)) | ||
def test_ISO4217_code_ok(currency: str): | ||
model = ISO4217CheckingModel(currency=currency) | ||
assert model.currency == currency | ||
assert model.model_dump() == {'currency': currency} # test serialization | ||
|
||
|
||
@pytest.mark.parametrize( | ||
'currency', | ||
filter( | ||
lambda code: code not in currency_code._CODES_FOR_BONDS_METAL_TESTING, | ||
map(lambda code: code.alpha_3, pycountry.currencies), | ||
), | ||
) | ||
def test_everyday_code_ok(currency: str): | ||
model = CurrencyCheckingModel(currency=currency) | ||
assert model.currency == currency | ||
assert model.model_dump() == {'currency': currency} # test serialization | ||
|
||
|
||
def test_ISO4217_fails(): | ||
with pytest.raises( | ||
ValidationError, | ||
match=re.escape( | ||
'1 validation error for ISO4217CheckingModel\ncurrency\n ' | ||
'Invalid ISO 4217 currency code. See https://en.wikipedia.org/wiki/ISO_4217 ' | ||
"[type=ISO4217, input_value='OMG', input_type=str]" | ||
), | ||
): | ||
ISO4217CheckingModel(currency='OMG') | ||
|
||
|
||
@pytest.mark.parametrize('forbidden_currency', forbidden_currencies) | ||
def test_forbidden_everyday(forbidden_currency): | ||
with pytest.raises( | ||
ValidationError, | ||
match=re.escape( | ||
'1 validation error for CurrencyCheckingModel\ncurrency\n ' | ||
'Invalid currency code. See https://en.wikipedia.org/wiki/ISO_4217. ' | ||
'Bonds, testing and precious metals codes are not allowed. ' | ||
f"[type=InvalidCurrency, input_value='{forbidden_currency}', input_type=str]" | ||
), | ||
): | ||
CurrencyCheckingModel(currency=forbidden_currency) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters