From e9ac8992324d016f78c1076dd4029fdfbb0ee4e2 Mon Sep 17 00:00:00 2001 From: willkraemer Date: Mon, 27 Oct 2025 17:39:27 -0300 Subject: [PATCH 1/7] feat(cnh): add CNH validation function and corresponding tests --- brutils/__init__.py | 5 +++ brutils/cnh.py | 83 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_cnh.py | 16 +++++++++ 3 files changed, 104 insertions(+) create mode 100644 brutils/cnh.py create mode 100644 tests/test_cnh.py diff --git a/brutils/__init__.py b/brutils/__init__.py index e0f75d3..bb38ace 100644 --- a/brutils/__init__.py +++ b/brutils/__init__.py @@ -20,6 +20,9 @@ from brutils.cpf import is_valid as is_valid_cpf from brutils.cpf import remove_symbols as remove_symbols_cpf +#CNH Imports +from brutils.cnh import is_valid_cnh as is_valid_cnh + # Currency from brutils.currency import convert_real_to_text, format_currency @@ -91,6 +94,8 @@ "generate_cpf", "is_valid_cpf", "remove_symbols_cpf", + #CNH + "is_valid_cnh", # Email "is_valid_email", # Legal Process diff --git a/brutils/cnh.py b/brutils/cnh.py new file mode 100644 index 0000000..1c7a47e --- /dev/null +++ b/brutils/cnh.py @@ -0,0 +1,83 @@ + + + + +def is_valid_cnh(cnh: str | None) -> bool: + """ + Validates the registration number for the Brazilian CNH (Carteira Nacional de Habilitação) that was created in 2022. + This function checks if the given CNH is valid based on the format and allowed characters, + verifying the registration and verification digits. + + Args: + cnh (str | None): CNH string (symbols will be ignored). + + Returns: + bool: True if CNH has a valid format. + """ + + cnh_digits, verification_cnh_digits = _get_cnh_number_and_last_two_digits(cnh) + + if not cnh: + return False + + if len(cnh_digits ) != 11: + return False + + # Reject sequences as "00000000000", "11111111111", etc. + if cnh_digits == cnh_digits[0] * 11: + return False + + digits = _get_cnh_digits(cnh_digits) + + first_validation_digit = _get_tenth_digit(digits, cnh_digits) + + if cnh_digits[9] != str(first_validation_digit): # checking the 10th digit + return False + + second_validation_digit = _get_eleventh_digit(digits, cnh_digits, first_validation_digit) + + return _validate_cnh(verification_cnh_digits, first_validation_digit, second_validation_digit) + +def _get_cnh_number_and_last_two_digits(cnh): + # removing symbols + cnh_digits = ''.join(filter(str.isdigit, cnh)) + verification_cnh_digits = cnh_digits[9:11] + + return cnh_digits, verification_cnh_digits + +def _get_cnh_digits(cnh_digits): + # get digits as list of integers + return [int(ch) for ch in cnh_digits ] + +def _get_tenth_digit(digits, cnh_digits): + ''' generating the first verification digit, which is the 10th digit of the CNH ''' + + sum_first = 0 + for i in range(9): + sum_first += digits[i] * (9 - i) + + sum_first = sum_first % 11 + first_validation_digit = 0 if sum_first > 9 else sum_first + + return first_validation_digit + +def _get_eleventh_digit(digits, cnh_digits, first_validation_digit): + ''' generating the second verification digit, which is the 11th digit of the CNH ''' + sum_second = 0 + for i in range(9): + sum_second += digits[i] * (i + 1) + + second_validation_digit = sum_second % 11 + + if first_validation_digit > 9: + second_validation_digit = second_validation_digit + 9 if (second_validation_digit - 2) < 0 else second_validation_digit - 2 + + if second_validation_digit > 9: + second_validation_digit = 0 + + return second_validation_digit + +def _validate_cnh(verification_cnh_digits, first_validation_digit, second_validation_digit): + ''' comparing the CNH verification digits with the generated verification digits ''' + calculated_cnh_digits = [str(first_validation_digit), str(second_validation_digit)] + return verification_cnh_digits == calculated_cnh_digits \ No newline at end of file diff --git a/tests/test_cnh.py b/tests/test_cnh.py new file mode 100644 index 0000000..1c4c1ea --- /dev/null +++ b/tests/test_cnh.py @@ -0,0 +1,16 @@ +from unittest import TestCase, main +from unittest.mock import MagicMock, patch + +from brutils.cnh import is_valid_cnh + + +class TestCNH(TestCase): + def test_is_valid_cnh(self): + self.assertFalse(is_valid_cnh("22222222222")) + self.assertFalse(is_valid_cnh("ABC70304734")) + self.assertFalse(is_valid_cnh("6619558737912")) + self.assertTrue(is_valid_cnh("097703047-34")) + self.assertTrue(is_valid_cnh("09770304734")) + + + From 128e7719898ba8334cccae1a7ac2af5cb39b474f Mon Sep 17 00:00:00 2001 From: willkraemer Date: Mon, 27 Oct 2025 17:40:21 -0300 Subject: [PATCH 2/7] fixed formatting --- brutils/__init__.py | 8 +++--- brutils/cnh.py | 66 ++++++++++++++++++++++++++++----------------- tests/test_cnh.py | 6 +---- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/brutils/__init__.py b/brutils/__init__.py index bb38ace..c61c052 100644 --- a/brutils/__init__.py +++ b/brutils/__init__.py @@ -8,6 +8,9 @@ from brutils.cep import is_valid as is_valid_cep from brutils.cep import remove_symbols as remove_symbols_cep +# CNH Imports +from brutils.cnh import is_valid_cnh as is_valid_cnh + # CNPJ Imports from brutils.cnpj import format_cnpj from brutils.cnpj import generate as generate_cnpj @@ -20,9 +23,6 @@ from brutils.cpf import is_valid as is_valid_cpf from brutils.cpf import remove_symbols as remove_symbols_cpf -#CNH Imports -from brutils.cnh import is_valid_cnh as is_valid_cnh - # Currency from brutils.currency import convert_real_to_text, format_currency @@ -94,7 +94,7 @@ "generate_cpf", "is_valid_cpf", "remove_symbols_cpf", - #CNH + # CNH "is_valid_cnh", # Email "is_valid_email", diff --git a/brutils/cnh.py b/brutils/cnh.py index 1c7a47e..94305b2 100644 --- a/brutils/cnh.py +++ b/brutils/cnh.py @@ -1,7 +1,3 @@ - - - - def is_valid_cnh(cnh: str | None) -> bool: """ Validates the registration number for the Brazilian CNH (Carteira Nacional de Habilitação) that was created in 2022. @@ -15,15 +11,17 @@ def is_valid_cnh(cnh: str | None) -> bool: bool: True if CNH has a valid format. """ - cnh_digits, verification_cnh_digits = _get_cnh_number_and_last_two_digits(cnh) + cnh_digits, verification_cnh_digits = _get_cnh_number_and_last_two_digits( + cnh + ) if not cnh: return False - if len(cnh_digits ) != 11: + if len(cnh_digits) != 11: return False - # Reject sequences as "00000000000", "11111111111", etc. + # Reject sequences as "00000000000", "11111111111", etc. if cnh_digits == cnh_digits[0] * 11: return False @@ -31,53 +29,71 @@ def is_valid_cnh(cnh: str | None) -> bool: first_validation_digit = _get_tenth_digit(digits, cnh_digits) - if cnh_digits[9] != str(first_validation_digit): # checking the 10th digit + if cnh_digits[9] != str(first_validation_digit): # checking the 10th digit return False - second_validation_digit = _get_eleventh_digit(digits, cnh_digits, first_validation_digit) + second_validation_digit = _get_eleventh_digit( + digits, cnh_digits, first_validation_digit + ) + + return _validate_cnh( + verification_cnh_digits, first_validation_digit, second_validation_digit + ) - return _validate_cnh(verification_cnh_digits, first_validation_digit, second_validation_digit) def _get_cnh_number_and_last_two_digits(cnh): # removing symbols - cnh_digits = ''.join(filter(str.isdigit, cnh)) + cnh_digits = "".join(filter(str.isdigit, cnh)) verification_cnh_digits = cnh_digits[9:11] return cnh_digits, verification_cnh_digits + def _get_cnh_digits(cnh_digits): - # get digits as list of integers - return [int(ch) for ch in cnh_digits ] + # get digits as list of integers + return [int(ch) for ch in cnh_digits] + def _get_tenth_digit(digits, cnh_digits): - ''' generating the first verification digit, which is the 10th digit of the CNH ''' + """generating the first verification digit, which is the 10th digit of the CNH""" sum_first = 0 for i in range(9): sum_first += digits[i] * (9 - i) - + sum_first = sum_first % 11 first_validation_digit = 0 if sum_first > 9 else sum_first - + return first_validation_digit + def _get_eleventh_digit(digits, cnh_digits, first_validation_digit): - ''' generating the second verification digit, which is the 11th digit of the CNH ''' + """generating the second verification digit, which is the 11th digit of the CNH""" sum_second = 0 for i in range(9): sum_second += digits[i] * (i + 1) - + second_validation_digit = sum_second % 11 if first_validation_digit > 9: - second_validation_digit = second_validation_digit + 9 if (second_validation_digit - 2) < 0 else second_validation_digit - 2 + second_validation_digit = ( + second_validation_digit + 9 + if (second_validation_digit - 2) < 0 + else second_validation_digit - 2 + ) if second_validation_digit > 9: - second_validation_digit = 0 - + second_validation_digit = 0 + return second_validation_digit -def _validate_cnh(verification_cnh_digits, first_validation_digit, second_validation_digit): - ''' comparing the CNH verification digits with the generated verification digits ''' - calculated_cnh_digits = [str(first_validation_digit), str(second_validation_digit)] - return verification_cnh_digits == calculated_cnh_digits \ No newline at end of file + +def _validate_cnh( + verification_cnh_digits, first_validation_digit, second_validation_digit +): + """comparing the CNH verification digits with the generated verification digits""" + calculated_cnh_digits = [ + str(first_validation_digit), + str(second_validation_digit), + ] + return verification_cnh_digits == calculated_cnh_digits diff --git a/tests/test_cnh.py b/tests/test_cnh.py index 1c4c1ea..a1049be 100644 --- a/tests/test_cnh.py +++ b/tests/test_cnh.py @@ -1,5 +1,4 @@ -from unittest import TestCase, main -from unittest.mock import MagicMock, patch +from unittest import TestCase from brutils.cnh import is_valid_cnh @@ -11,6 +10,3 @@ def test_is_valid_cnh(self): self.assertFalse(is_valid_cnh("6619558737912")) self.assertTrue(is_valid_cnh("097703047-34")) self.assertTrue(is_valid_cnh("09770304734")) - - - From b0abef8bca16181522e935d49f231214e4d959b4 Mon Sep 17 00:00:00 2001 From: willkraemer Date: Tue, 28 Oct 2025 07:30:10 -0300 Subject: [PATCH 3/7] refactor cnh.py --- brutils/cnh.py | 99 +++++++++++++++++--------------------------------- 1 file changed, 34 insertions(+), 65 deletions(-) diff --git a/brutils/cnh.py b/brutils/cnh.py index 94305b2..d08e5ec 100644 --- a/brutils/cnh.py +++ b/brutils/cnh.py @@ -1,99 +1,68 @@ -def is_valid_cnh(cnh: str | None) -> bool: +def is_valid_cnh(cnh: str) -> bool: """ Validates the registration number for the Brazilian CNH (Carteira Nacional de Habilitação) that was created in 2022. + Previous versions of the CNH are not in this version. This function checks if the given CNH is valid based on the format and allowed characters, - verifying the registration and verification digits. + verifying the verification digits. Args: - cnh (str | None): CNH string (symbols will be ignored). + cnh (str): CNH string (symbols will be ignored). Returns: bool: True if CNH has a valid format. """ - - cnh_digits, verification_cnh_digits = _get_cnh_number_and_last_two_digits( - cnh - ) + cnh = "".join(filter(str.isdigit, cnh)) # clean the input and check for numbers only if not cnh: return False - if len(cnh_digits) != 11: + if len(cnh) != 11: return False # Reject sequences as "00000000000", "11111111111", etc. - if cnh_digits == cnh_digits[0] * 11: + if cnh == cnh[0] * 11: return False - digits = _get_cnh_digits(cnh_digits) - - first_validation_digit = _get_tenth_digit(digits, cnh_digits) + # cast digits as list of integers + digits = [int(ch) for ch in cnh] + first_verificator = digits[9] + second_verificator = digits[10] - if cnh_digits[9] != str(first_validation_digit): # checking the 10th digit + if not _check_first_verificator(digits, first_verificator): # checking the 10th digit, if not already invalid return False + + return _check_second_verificator(digits, second_verificator, first_verificator) # checking the 11th digit - second_validation_digit = _get_eleventh_digit( - digits, cnh_digits, first_validation_digit - ) - - return _validate_cnh( - verification_cnh_digits, first_validation_digit, second_validation_digit - ) - - -def _get_cnh_number_and_last_two_digits(cnh): - # removing symbols - cnh_digits = "".join(filter(str.isdigit, cnh)) - verification_cnh_digits = cnh_digits[9:11] - - return cnh_digits, verification_cnh_digits +def _check_first_verificator(digits : list[int], first_verificator : int) -> bool: + """ generating the first verification digit, which is used to verify the 10th digit of the CNH""" -def _get_cnh_digits(cnh_digits): - # get digits as list of integers - return [int(ch) for ch in cnh_digits] - - -def _get_tenth_digit(digits, cnh_digits): - """generating the first verification digit, which is the 10th digit of the CNH""" - - sum_first = 0 + sum = 0 for i in range(9): - sum_first += digits[i] * (9 - i) + sum += digits[i] * (9 - i) - sum_first = sum_first % 11 - first_validation_digit = 0 if sum_first > 9 else sum_first + sum = sum % 11 + result = 0 if sum > 9 else sum - return first_validation_digit + return result == first_verificator -def _get_eleventh_digit(digits, cnh_digits, first_validation_digit): - """generating the second verification digit, which is the 11th digit of the CNH""" - sum_second = 0 +def _check_second_verificator(digits : list[int], second_verificator : int, first_verificator : int) -> bool: + """ generating the second verification digit, which is used to verify the 11th digit of the CNH """ + sum = 0 for i in range(9): - sum_second += digits[i] * (i + 1) + sum += digits[i] * (i + 1) - second_validation_digit = sum_second % 11 + second_verificator = sum % 11 - if first_validation_digit > 9: - second_validation_digit = ( - second_validation_digit + 9 - if (second_validation_digit - 2) < 0 - else second_validation_digit - 2 + if first_verificator > 9: + second_verificator = ( + second_verificator + 9 + if (second_verificator - 2) < 0 + else second_verificator - 2 ) - if second_validation_digit > 9: - second_validation_digit = 0 - - return second_validation_digit - + if second_verificator > 9: + second_verificator = 0 -def _validate_cnh( - verification_cnh_digits, first_validation_digit, second_validation_digit -): - """comparing the CNH verification digits with the generated verification digits""" - calculated_cnh_digits = [ - str(first_validation_digit), - str(second_validation_digit), - ] - return verification_cnh_digits == calculated_cnh_digits + return second_verificator From cb174992f296a4f833ce0e292af841ee448ca8b5 Mon Sep 17 00:00:00 2001 From: willkraemer Date: Tue, 28 Oct 2025 07:35:54 -0300 Subject: [PATCH 4/7] improve naming --- brutils/cnh.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/brutils/cnh.py b/brutils/cnh.py index d08e5ec..139c627 100644 --- a/brutils/cnh.py +++ b/brutils/cnh.py @@ -23,8 +23,8 @@ def is_valid_cnh(cnh: str) -> bool: if cnh == cnh[0] * 11: return False - # cast digits as list of integers - digits = [int(ch) for ch in cnh] + # cast digits to list of integers + digits : list[int] = [int(ch) for ch in cnh] first_verificator = digits[9] second_verificator = digits[10] @@ -35,7 +35,9 @@ def is_valid_cnh(cnh: str) -> bool: def _check_first_verificator(digits : list[int], first_verificator : int) -> bool: - """ generating the first verification digit, which is used to verify the 10th digit of the CNH""" + """ + Generates the first verification digit and uses it to verify the 10th digit of the CNH + """ sum = 0 for i in range(9): @@ -48,21 +50,23 @@ def _check_first_verificator(digits : list[int], first_verificator : int) -> boo def _check_second_verificator(digits : list[int], second_verificator : int, first_verificator : int) -> bool: - """ generating the second verification digit, which is used to verify the 11th digit of the CNH """ + """ + Generates the second verification and uses it to verify the 11th digit of the CNH + """ sum = 0 for i in range(9): sum += digits[i] * (i + 1) - second_verificator = sum % 11 + result = sum % 11 if first_verificator > 9: - second_verificator = ( - second_verificator + 9 - if (second_verificator - 2) < 0 - else second_verificator - 2 + result = ( + result + 9 + if (result - 2) < 0 + else result - 2 ) - if second_verificator > 9: - second_verificator = 0 + if result > 9: + result = 0 - return second_verificator + return result == second_verificator From 894ac88e9aee6329640a9068815e86e470e5b298 Mon Sep 17 00:00:00 2001 From: willkraemer Date: Tue, 28 Oct 2025 07:39:34 -0300 Subject: [PATCH 5/7] fi format --- brutils/cnh.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/brutils/cnh.py b/brutils/cnh.py index 139c627..3d31ff0 100644 --- a/brutils/cnh.py +++ b/brutils/cnh.py @@ -11,7 +11,9 @@ def is_valid_cnh(cnh: str) -> bool: Returns: bool: True if CNH has a valid format. """ - cnh = "".join(filter(str.isdigit, cnh)) # clean the input and check for numbers only + cnh = "".join( + filter(str.isdigit, cnh) + ) # clean the input and check for numbers only if not cnh: return False @@ -24,19 +26,23 @@ def is_valid_cnh(cnh: str) -> bool: return False # cast digits to list of integers - digits : list[int] = [int(ch) for ch in cnh] + digits: list[int] = [int(ch) for ch in cnh] first_verificator = digits[9] second_verificator = digits[10] - if not _check_first_verificator(digits, first_verificator): # checking the 10th digit, if not already invalid + if not _check_first_verificator( + digits, first_verificator + ): # checking the 10th digit return False - - return _check_second_verificator(digits, second_verificator, first_verificator) # checking the 11th digit + return _check_second_verificator( + digits, second_verificator, first_verificator + ) # checking the 11th digit -def _check_first_verificator(digits : list[int], first_verificator : int) -> bool: - """ - Generates the first verification digit and uses it to verify the 10th digit of the CNH + +def _check_first_verificator(digits: list[int], first_verificator: int) -> bool: + """ + Generates the first verification digit and uses it to verify the 10th digit of the CNH """ sum = 0 @@ -49,9 +55,11 @@ def _check_first_verificator(digits : list[int], first_verificator : int) -> boo return result == first_verificator -def _check_second_verificator(digits : list[int], second_verificator : int, first_verificator : int) -> bool: - """ - Generates the second verification and uses it to verify the 11th digit of the CNH +def _check_second_verificator( + digits: list[int], second_verificator: int, first_verificator: int +) -> bool: + """ + Generates the second verification and uses it to verify the 11th digit of the CNH """ sum = 0 for i in range(9): @@ -60,11 +68,7 @@ def _check_second_verificator(digits : list[int], second_verificator : int, firs result = sum % 11 if first_verificator > 9: - result = ( - result + 9 - if (result - 2) < 0 - else result - 2 - ) + result = result + 9 if (result - 2) < 0 else result - 2 if result > 9: result = 0 From ccdd691b2b3800e9582e36acc38594aa487943c6 Mon Sep 17 00:00:00 2001 From: William Kraemer Aliaga Date: Wed, 29 Oct 2025 19:44:06 -0300 Subject: [PATCH 6/7] Add documentation --- README.md | 33 +++++++++++++++++++++++++++++++++ README_EN.md | 33 +++++++++++++++++++++++++++++++++ brutils/cnh.py | 12 +++++++++++- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b1e468..681e039 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ False - [is\_valid\_email](#is_valid_email) - [Data](#date) - [convert\_date\_to_text](#convert_date_to_text) +- [CNH](#cnh) + - [is\_valid\_cnh](#is_valid_cnh) - [Placa de Carro](#placa-de-carro) - [is\_valid\_license\_plate](#is_valid_license_plate) - [format\_license\_plate](#format_license_plate) @@ -665,6 +667,37 @@ None "Primeiro de agosto de dois mil e vinte e quatro" ```` +## CNH + +### is_valid_cnh + +Verifica se o número de registro de CNH (Carteira de Habilitação Nacional) brasileiro é válido. +Para que um número de CNH seja considerado válido, a entrada deve ser uma string contendo +exatamente 11 dígitos numéricos. Esta função não verifica se o número da CNH é real, apenas +valida os dígitos verificadores. + +Argumentos: + +- cnh (str): A string contendo o número de registro de CNH a ser verificado. + +Retorno: + +- bool: True se o número de registro da CNHN for válido (11 dígitos), False caso contrário. + +Exemplo: + +```python +>>> from brutils import is_valid_cnh +>>> is_valid_cnh("12345678901") +False +>>> is_valid_cnh("A2C45678901") +False +>>> is_valid_cnh("98765432100") +True +>>> is_valid_cnh("987654321-00") +True +``` + ## Placa de Carro diff --git a/README_EN.md b/README_EN.md index 32017ea..1096ac8 100644 --- a/README_EN.md +++ b/README_EN.md @@ -68,6 +68,8 @@ False - [generate\_phone](#generate_phone) - [Email](#email) - [is\_valid\_email](#is_valid_email) +- [CNH](#cnh) + - [is\_valid\_cnh](#is_valid_cnh) - [License Plate](#license-plate) - [is\_valid\_license\_plate](#is_valid_license_plate) - [format\_license\_plate](#format_license_plate) @@ -658,6 +660,37 @@ False False ``` + +## CNH + +### is_valid_cnh + +Checks if the registration number of a brazilian CNH (Carteira de Habilitação Nacional or Driver's License in En.) is valid. +To be considered valid, the input must be a string containing exactly 11 digits. This function does not verify if the registration number of the CNH is a real one, it only validates it's verification numbers. + +Argumentos: + +- cnh (str): A string containing the registration nunber of the CNH to be checked. + +Retorno: + +- bool: True if the CNH number is valid (11 digits), False otherwise. + +Exemplo: + +```python +>>> from brutils import is_valid_cnh +>>> is_valid_cnh("123456789") +False +>>> is_valid_cnh("A2C45678901") +False +>>> is_valid_cnh("98765432100") +True +>>> is_valid_cnh("987654321-00") +True +``` + + ## License Plate ### is_valid_license_plate diff --git a/brutils/cnh.py b/brutils/cnh.py index 3d31ff0..f7347ac 100644 --- a/brutils/cnh.py +++ b/brutils/cnh.py @@ -1,7 +1,7 @@ def is_valid_cnh(cnh: str) -> bool: """ Validates the registration number for the Brazilian CNH (Carteira Nacional de Habilitação) that was created in 2022. - Previous versions of the CNH are not in this version. + Previous versions of the CNH are not supported in this version. This function checks if the given CNH is valid based on the format and allowed characters, verifying the verification digits. @@ -10,6 +10,16 @@ def is_valid_cnh(cnh: str) -> bool: Returns: bool: True if CNH has a valid format. + + Examples: + >>> is_valid_cnh("12345678901") + False + >>> is_valid_cnh("A2C45678901") + False + >>> is_valid_cnh("98765432100") + True + >>> is_valid_cnh("987654321-00") + True """ cnh = "".join( filter(str.isdigit, cnh) From 3345377665bddd63411158866ea13023cc04ea10 Mon Sep 17 00:00:00 2001 From: William Kraemer Aliaga Date: Wed, 29 Oct 2025 19:52:31 -0300 Subject: [PATCH 7/7] Added changelog entries --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1987351..208e261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Utilitário `convert_name_to_uf` +- Utilitário `is_valid_cnh` [#651](https://github.com/brazilian-utils/brutils-python/pull/651) ## [2.3.0] - 2025-10-07