diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27058df..48d6613 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2.2.1 with: - python-version: 3.8 + python-version: "3.8" - name: Install dependencies run: make install-test - name: Lint @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/Makefile b/Makefile index d5f48fa..7cb9232 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ SHELL := bash PATH := ./venv/bin:${PATH} -PYTHON = python3.7 +PYTHON = python3.8 PROJECT = clabe isort = isort $(PROJECT) tests setup.py black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py diff --git a/README.md b/README.md index 78f1051..a760c53 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ https://es.wikipedia.org/wiki/CLABE ## Requerimientos -Python 3.6 o superior. +Python 3.8 o superior. ## Instalación diff --git a/clabe/errors.py b/clabe/errors.py deleted file mode 100644 index 6ecd610..0000000 --- a/clabe/errors.py +++ /dev/null @@ -1,11 +0,0 @@ -from pydantic.errors import PydanticValueError - - -class BankCodeValidationError(PydanticValueError): - code = 'clabe.bank_code' - msg_template = 'código de banco no es válido' - - -class ClabeControlDigitValidationError(PydanticValueError): - code = 'clabe.control_digit' - msg_template = 'clabe dígito de control no es válido' diff --git a/clabe/types.py b/clabe/types.py index 08d6052..5e3fc5f 100644 --- a/clabe/types.py +++ b/clabe/types.py @@ -1,24 +1,10 @@ -from typing import TYPE_CHECKING, ClassVar +from typing import Any, ClassVar, Dict, Type -from pydantic.errors import NotDigitError -from pydantic.validators import ( - constr_length_validator, - constr_strip_whitespace, - str_validator, -) +from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler +from pydantic_core import PydanticCustomError, core_schema -from .errors import BankCodeValidationError, ClabeControlDigitValidationError from .validations import BANK_NAMES, BANKS, compute_control_digit -if TYPE_CHECKING: - from pydantic.typing import CallableGenerator - - -def validate_digits(v: str) -> str: - if not v.isdigit(): - raise NotDigitError - return v - class Clabe(str): """ @@ -29,33 +15,55 @@ class Clabe(str): min_length: ClassVar[int] = 18 max_length: ClassVar[int] = 18 - def __init__(self, clabe: str): + def __init__(self, clabe: str) -> None: self.bank_code_abm = clabe[:3] self.bank_code_banxico = BANKS[clabe[:3]] self.bank_name = BANK_NAMES[self.bank_code_banxico] + @property + def bank_code(self) -> str: + return self.bank_code_banxico + @classmethod - def __get_validators__(cls) -> 'CallableGenerator': - yield str_validator - yield constr_strip_whitespace - yield constr_length_validator - yield validate_digits - yield cls.validate_bank_code_abm - yield cls.validate_control_digit - yield cls + def __get_pydantic_json_schema__( + cls, + schema: core_schema.CoreSchema, + handler: GetJsonSchemaHandler, + ) -> Dict[str, Any]: + json_schema = handler(schema) + json_schema.update( + type="string", + pattern="^[0-9]{18}$", + description="CLABE (Clave Bancaria Estandarizada)", + examples=["723010123456789019"], + ) + return json_schema @classmethod - def validate_bank_code_abm(cls, clabe: str) -> str: - if clabe[:3] not in BANKS.keys(): - raise BankCodeValidationError - return clabe + def __get_pydantic_core_schema__( + cls, + _: Type[Any], + __: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + return core_schema.no_info_after_validator_function( + cls._validate, + core_schema.str_schema( + min_length=cls.min_length, + max_length=cls.max_length, + strip_whitespace=cls.strip_whitespace, + ), + ) @classmethod - def validate_control_digit(cls, clabe: str) -> str: + def _validate(cls, clabe: str) -> 'Clabe': + if not clabe.isdigit(): + raise PydanticCustomError('clabe', 'debe ser numérico') + if clabe[:3] not in BANKS: + raise PydanticCustomError( + 'clabe.bank_code', 'código de banco no es válido' + ) if clabe[-1] != compute_control_digit(clabe): - raise ClabeControlDigitValidationError - return clabe - - @property - def bank_code(self): - return self.bank_code_banxico + raise PydanticCustomError( + 'clabe.control_digit', 'clabe dígito de control no es válido' + ) + return cls(clabe) diff --git a/clabe/version.py b/clabe/version.py index dee93a9..8933c93 100644 --- a/clabe/version.py +++ b/clabe/version.py @@ -1 +1 @@ -__version__ = '1.2.16' +__version__ = '2.0.0.dev0' diff --git a/requirements-test.txt b/requirements-test.txt index 3d06f76..a02d640 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,4 +3,4 @@ pytest-cov==2.11.* black==22.3.0 isort==5.10.* flake8==4.0.* -mypy==0.790 +mypy==1.13.0 diff --git a/requirements.txt b/requirements.txt index 62c77cd..e142a8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pydantic==1.9.0 +pydantic==2.10.3 diff --git a/setup.py b/setup.py index 80d79db..fdee387 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ packages=setuptools.find_packages(), include_package_data=True, package_data=dict(clabe=['py.typed']), - install_requires=['pydantic>=1.4,<2.0'], + install_requires=['pydantic>=2.10.3'], classifiers=[ 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', diff --git a/tests/test_types.py b/tests/test_types.py index 543ea23..a23a358 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,13 +1,8 @@ import pytest -from pydantic import BaseModel -from pydantic.errors import NotDigitError +from pydantic import BaseModel, ValidationError -from clabe import BANK_NAMES, BANKS, compute_control_digit -from clabe.errors import ( - BankCodeValidationError, - ClabeControlDigitValidationError, -) -from clabe.types import Clabe, validate_digits +from clabe import BANK_NAMES, BANKS +from clabe.types import Clabe VALID_CLABE = '646180157042875763' @@ -24,23 +19,47 @@ def test_valid_clabe(): assert cuenta.clabe.bank_code == cuenta.clabe.bank_code_banxico -def test_clabe_digits(): - assert validate_digits(VALID_CLABE) - - -def test_clabe_not_digit(): - with pytest.raises(NotDigitError): - validate_digits('h' * 18) - +@pytest.mark.parametrize( + 'clabe,expected_message', + [ + pytest.param( + 'h' * 18, + 'debe ser numérico', + id='clabe_not_digit', + ), + pytest.param( + '9' * 17, + 'String should have at least 18 characters', + id='invalid_bank_code_abm', + ), + pytest.param( + '111180157042875763', + 'código de banco no es válido', + id='invalid_bank_code', + ), + pytest.param( + '001' + '9' * 15, + 'clabe dígito de control no es válido', + id='invalid_control_digit', + ), + ], +) +def test_invalid_clabe(clabe: Clabe, expected_message: str) -> None: + with pytest.raises(ValidationError) as exc: + Cuenta(clabe=clabe) + assert expected_message in str(exc.value) -def test_invalid_bank_code_abm(): - clabe = '9' * 17 - clabe += compute_control_digit(clabe) - with pytest.raises(BankCodeValidationError): - Clabe.validate_bank_code_abm(clabe) +def test_get_json_schema() -> None: + from pydantic import TypeAdapter -def test_invalid_control_digit(): - clabe = '001' + '9' * 15 - with pytest.raises(ClabeControlDigitValidationError): - Clabe.validate_control_digit(clabe) + adapter = TypeAdapter(Clabe) + schema = adapter.json_schema() + assert schema == { + 'description': 'CLABE (Clave Bancaria Estandarizada)', + 'examples': ['723010123456789019'], + 'maxLength': 18, + 'minLength': 18, + 'pattern': '^[0-9]{18}$', + 'type': 'string', + }