Skip to content

Commit

Permalink
Update/pydantic v2 (#171)
Browse files Browse the repository at this point in the history
* Upgrade Python version requirements and dependencies

- Updated Python version from 3.7 to 3.8 in Makefile and README.md.
- Upgraded Pydantic dependency from 1.9.0 to 2.10.3 in requirements.txt and setup.py.
- Updated mypy version from 0.790 to 1.13.0 in requirements-test.txt.
- Refactored CLABE validation logic in types.py, removing custom error classes and integrating Pydantic's validation features.
- Removed unused error handling code and updated tests to reflect changes in validation logic.
- Updated GitHub Actions workflow to support Python versions 3.8 through 3.13.
- Bumped version to 2.0.0.dev0 in version.py.

* Update GitHub Actions workflow to use string format for Python versions

* Update GitHub Actions workflow to include Python 3.13 in the testing

* Add python version actions (#172)

* Add python version actions

* Update test.yml

* update test deps

---------

Co-authored-by: Felipe López <[email protected]>

* Upgrade Python version requirements and dependencies

- Updated Python version from 3.7 to 3.8 in Makefile and README.md.
- Upgraded Pydantic dependency from 1.9.0 to 2.10.3 in requirements.txt and setup.py.
- Updated mypy version from 0.790 to 1.13.0 in requirements-test.txt.
- Refactored CLABE validation logic in types.py, removing custom error classes and integrating Pydantic's validation features.
- Removed unused error handling code and updated tests to reflect changes in validation logic.
- Updated GitHub Actions workflow to support Python versions 3.8 through 3.13.
- Bumped version to 2.0.0.dev0 in version.py.

* Changed regex to pattern in BankConfigRequest model

* Update README; bump version to 2.0.0

* Update GitHub Actions workflow to remove Python 3.7 from the testing matrix, supporting versions 3.8 through 3.13.

* This change enhances compatibility with Pydantic V2 and streamlines error handling.

* Update setup.py to require Python 3.8 and bump version to 2.0.0

* Enhance README with Pydantic v2 usage examples and update CLABE validation logic

* Fix code block formatting in README.md for Pydantic example

* Removing redundant test case

---------

Co-authored-by: gabino <[email protected]>
Co-authored-by: Pach <[email protected]>
Co-authored-by: Felipe López <[email protected]>
  • Loading branch information
4 people authored Dec 30, 2024
1 parent e8cbcaa commit 8aea318
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
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 }}
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
56 changes: 47 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ https://es.wikipedia.org/wiki/CLABE

## Requerimientos

Python 3.6 o superior.
Python 3.8 o superior.

## Instalación

Expand All @@ -30,48 +30,86 @@ $ make test

## Uso básico

Obtener el dígito de control de un número CLABE
### Como tipo personalizado en un modelo de Pydantic

```python
from pydantic import BaseModel, ValidationError

from clabe import Clabe


class Account(BaseModel):
id: str
clabe: Clabe


account = Account(id='123', clabe='723010123456789019')
print(account)
"""
id='123' clabe='723010123456789019'
"""

try:
account = Account(id='321', clabe='000000000000000011')
except ValidationError as exc:
print(exc)
"""
1 validation error for Account
clabe
código de banco no es válido [type=clabe.bank_code, input_value='000000000000000011', input_type=str]
"""
```

### Obtener el dígito de control de un número CLABE

```python
import clabe
clabe.compute_control_digit('00200000000000000')
```

Para validar si un número CLABE es válido
### Para validar si un número CLABE es válido

```python
import clabe
clabe.validate_clabe('002000000000000008')
```

Para obtener el banco a partir de 3 dígitos
### Para obtener el banco a partir de 3 dígitos

```python
import clabe
clabe.get_bank_name('002')
```

Para generar nuevo válido CLABES
### Para generar nuevo válido CLABES

```python
import clabe
clabe.generate_new_clabes(10, '002123456')
```

## Para agregar un nuevo banco
## Agregar un nuevo banco

A partir de la versión **2.0.0**, el paquete ha sido actualizado para utilizar **Pydantic v2**, lo que implica que las versiones anteriores ya no recibirán soporte ni actualizaciones.

A partir de la versión 2.0.0, el paquete se actualizará a **Pydantic v2**, lo que significa que las versiones anteriores ya no recibirán soporte.
No obstante, en versiones anteriores hemos agregado una función que permite añadir bancos adicionales a la lista sin necesidad de crear un PR. Esto es útil para quienes aún utilicen versiones anteriores. Sin embargo, a partir de la versión 2, continuaremos manteniendo y actualizando la lista oficial de bancos mediante PRs en el repositorio.

Sin embargo, hemos añadido una función para agregar bancos adicionales a la lista, en caso de que sea necesario. Esto se puede hacer sin necesidad de crear un PR. Para agregar un banco, simplemente llama a la siguiente función con el código de Banxico y el nombre del banco:
### Cómo agregar un banco

Para agregar un banco, llama a la función `add_bank` pasando el código de Banxico y el nombre del banco como parámetros.

```python
import clabe
clabe.add_bank('12345', 'New Bank')
```

Para eliminar un banco
### Cómo eliminar un banco

De manera similar, puedes eliminar un banco llamando a la función remove_bank con el código del banco que deseas eliminar.

```python
import clabe
clabe.remove_bank('12345')
```

**Nota**: Aunque estas funciones están disponibles para un uso más flexible, recomendamos utilizar siempre la lista oficial de bancos actualizada en la versión 2+.
11 changes: 0 additions & 11 deletions clabe/errors.py

This file was deleted.

86 changes: 46 additions & 40 deletions clabe/types.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,67 @@
from typing import TYPE_CHECKING, ClassVar
from typing import Any, Dict, Type

from pydantic.v1.errors import NotDigitError
from pydantic.v1.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.v1.typing import CallableGenerator


def validate_digits(v: str) -> str:
if not v.isdigit():
raise NotDigitError
return v
CLABE_LENGTH = 18


class Clabe(str):
"""
Based on: https://es.wikipedia.org/wiki/CLABE
"""

strip_whitespace: ClassVar[bool] = True
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=CLABE_LENGTH,
max_length=CLABE_LENGTH,
strip_whitespace=True,
),
)

@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)
3 changes: 2 additions & 1 deletion clabe/validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class BankConfigRequest(BaseModel):
)

bank_code_banxico: str = Field(
regex=r"^\d{5}$", description="Banxico code must be a 5-digit string."
pattern=r"^\d{5}$",
description="Banxico code must be a 5-digit string.",
)

@property
Expand Down
2 changes: 1 addition & 1 deletion clabe/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.3.0'
__version__ = '2.0.0'
2 changes: 1 addition & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ pytest-cov==4.1.0
black==22.8.0
isort==5.11.5
flake8==5.0.4
mypy==1.4.1
mypy==1.4.1
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pydantic==1.10.19
pydantic==2.10.3
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
packages=setuptools.find_packages(),
include_package_data=True,
package_data=dict(clabe=['py.typed']),
install_requires=['pydantic>=1.10.17'],
python_requires='>=3.8',
install_requires=['pydantic>=2.10.3'],
classifiers=[
'Programming Language :: Python :: 3',
'License :: OSI Approved :: MIT License',
Expand Down
82 changes: 53 additions & 29 deletions tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import pytest
from pydantic.v1 import BaseModel
from pydantic.v1.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'
VALID_CLABE = '723123456682660854'


class Cuenta(BaseModel):
Expand All @@ -18,29 +13,58 @@ class Cuenta(BaseModel):

def test_valid_clabe():
cuenta = Cuenta(clabe=VALID_CLABE)
assert cuenta.clabe.bank_code_abm == '646'
assert cuenta.clabe.bank_code_banxico == BANKS['646']
assert cuenta.clabe.bank_name == BANK_NAMES[BANKS['646']]
assert cuenta.clabe.bank_code_abm == '723'
assert cuenta.clabe.bank_code_banxico == BANKS['723']
assert cuenta.clabe.bank_name == BANK_NAMES[BANKS['723']]
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_length',
),
pytest.param(
'9' * 19,
'String should have at most 18 characters',
id='invalid_bank_length',
),
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',
}

0 comments on commit 8aea318

Please sign in to comment.