Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pydantic v2 #163

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/licenses.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ jobs:
PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-[email protected]
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.8
- name: Install and run pip-licenses
run: |
pip config set global.extra-index-url https://$PYPI_USERNAME:[email protected]:8081
make install-test
pip install pip-licenses
pip-licenses --format=csv --output-file licence_compliance.csv
pdm run pip config set global.extra-index-url https://$PYPI_USERNAME:[email protected]:8081
rm -f .pdm-python
make install
pdm run pip install pip-licenses
pdm run pip-licenses --format=csv --output-file licence_compliance.csv
felipao-mx marked this conversation as resolved.
Show resolved Hide resolved
- name: Upload S3
# https://github.com/zdurham/s3-upload-github-action
run: |
Expand Down
13 changes: 6 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
name: release

on: push
on:
release:
types: [published]

jobs:
publish-pypi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Set up Python 3.8
uses: actions/setup-[email protected]
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.8
- name: Install dependencies
run: pip install -qU setuptools wheel twine
- name: Generating distribution archives
run: python setup.py sdist bdist_wheel
run: pdm build
- name: Publish distribution 📦 to PyPI
if: startsWith(github.event.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
Expand Down
34 changes: 20 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,55 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-[email protected]
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.8
- name: Install dependencies
run: make install-test
run: |
pdm use -f python3.8
make install
- name: Lint
run: make lint

pytest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8]
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-[email protected]
uses: pdm-project/setup-pdm@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: make install-test
run: |
pdm use -f python${{ matrix.python-version }}
make install
- name: Run tests
run: pytest
run: make test

coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-[email protected]
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.8
- name: Install dependencies
run: make install-test
run: |
pdm use -f python3.8
make install
- name: Generate coverage report
run: pytest --cov-report=xml
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2.1.0
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,7 @@ venv.bak/

# others
.DS_Store

# pdm
.pdm-python
.pdm-build/
50 changes: 32 additions & 18 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,48 @@ SHELL := bash
PATH := ./venv/bin:${PATH}
PYTHON = python3.7
PROJECT = clabe
isort = isort $(PROJECT) tests setup.py
black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py
isort = isort $(PROJECT) tests
black = black -S -l 79 --target-version py38 $(PROJECT) tests
PYDANTIC_V1_VENV := pydantic_v1


all: test

venv:
$(PYTHON) -m venv --prompt $(PROJECT) venv
pip install -qU pip
# Trying to run `pdm venv create` raises an error if the virtual env already exist.
# `pdm venv create` with --override option will clean all the packages installed in that venv.
@if pdm venv list | grep -q $(PYDANTIC_V1_VENV); then \
echo "Virtual environment $(PYDANTIC_V1_VENV) already exists. Skipping creation."; \
else \
echo "Creating virtual environment $(PYDANTIC_V1_VENV)."; \
pdm venv create --with-pip --name $(PYDANTIC_V1_VENV); \
fi
felipao-mx marked this conversation as resolved.
Show resolved Hide resolved
felipao-mx marked this conversation as resolved.
Show resolved Hide resolved

install:
pip install -qU -r requirements.txt
install: venv
# Installing packages defined in pyproject.toml as normal
pdm install -q
# Install old Pydantic package version in a new environment
pdm install -q --venv $(PYDANTIC_V1_VENV) --lockfile pdm-legacy.lock --override requirements-legacy.txt
felipao-mx marked this conversation as resolved.
Show resolved Hide resolved

install-test: install
pip install -qU -r requirements-test.txt

test: clean install-test lint
pytest
test: venv clean
# Runs pytest in each virtual environment and combine the coverage reports into one.
pdm run pytest
mv .coverage .cov.pydantic_v2
pdm run --venv $(PYDANTIC_V1_VENV) pytest
mv .coverage .cov.pydantic_v1
pdm run coverage combine .cov.pydantic_v1 .cov.pydantic_v2
pdm run coverage report -m
pdm run coverage xml -o coverage.xml
felipao-mx marked this conversation as resolved.
Show resolved Hide resolved

format:
$(isort)
$(black)
pdm run $(isort)
pdm run $(black)

lint:
flake8 $(PROJECT) tests setup.py
$(isort) --check-only
$(black) --check
mypy $(PROJECT) tests
pdm run flake8 $(PROJECT) tests
pdm run $(isort) --check-only
pdm run $(black) --check
pdm run mypy $(PROJECT) tests

clean:
rm -rf `find . -name __pycache__`
Expand All @@ -51,4 +65,4 @@ release: test clean
twine upload dist/*


.PHONY: all install-test test format lint clean release
.PHONY: all test format lint clean release
63 changes: 60 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
[![Downloads](https://pepy.tech/badge/clabe)](https://pepy.tech/project/clabe)

Librería para validar y calcular un número CLABE basado en
https://es.wikipedia.org/wiki/CLABE
[CLABE](https://es.wikipedia.org/wiki/CLABE).

Además, incluye la clase `Clabe`, un tipo personalizado diseñado
para integrarse con Pydantic, proporcionando un validador
robusto y eficiente para números CLABE dentro de tus
modelos Pydantic. Compatible con Pydantic V1.10.x y V2.x.x

## Requerimientos

Python 3.6 o superior.
Python 3.8 o superior.

## Instalación

Expand All @@ -22,9 +27,31 @@ pip install clabe

## Pruebas

Para ejecutar las pruebas
### Requisitos previos

#### Instalar PDM

Usamos PDM como administrador de paquetes y entornos virtuales (virtualenv).
Esto nos permite ejecutar pruebas unitarias con Pydantic
tanto en las versiones 1.x.x como 2.x.x. Sigue la [guía oficial](https://pdm-project.org/en/latest/#recommended-installation-method)
de instalación para instalarlo.

#### Instalar dependencias

El siguiente comando creará dos virtualenv. Uno donde se instala
pydantic V1.x.x y otro donde se instala Pydantic V2. Todo esto
es gestionado por PDM.

```bash
make install
```

### Ejecutar las pruebas

El siguiente comando ejecutará el conjunto de pruebas de ambos
virtualenv y generará un único reporte de pruebas y cobertura.

```bash
$ make test
```

Expand Down Expand Up @@ -57,3 +84,33 @@ Para generar nuevo válido CLABES
import clabe
clabe.generate_new_clabes(10, '002123456')
```

### 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]
"""
```
3 changes: 2 additions & 1 deletion clabe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
'validate_clabe',
]

from clabe.types import Clabe

from .banks import BANK_NAMES, BANKS
from .types import Clabe
from .validations import (
compute_control_digit,
generate_new_clabes,
Expand Down
8 changes: 8 additions & 0 deletions clabe/types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from ..utils import is_pydantic_v1

__all__ = ['Clabe']

if is_pydantic_v1():
from .clabes_legacy.clabes import Clabe # type: ignore
else:
from .clabes import Clabe # type: ignore
65 changes: 65 additions & 0 deletions clabe/types/clabes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from typing import Any, ClassVar, Dict, Type

from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic_core import PydanticCustomError, core_schema

from ..validations import BANK_NAMES, BANKS, compute_control_digit


class Clabe(str):
strip_whitespace: ClassVar[bool] = True
min_length: ClassVar[int] = 18
max_length: ClassVar[int] = 18

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]
felipao-mx marked this conversation as resolved.
Show resolved Hide resolved

@property
def bank_code(self) -> str:
return self.bank_code_banxico
felipao-mx marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
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 __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(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 PydanticCustomError(
'clabe.control_digit', 'clabe dígito de control no es válido'
)
return cls(clabe)
Empty file.
Loading