├── tests
├── __init__.py
├── ibge
│ ├── __init__.py
│ └── test_uf.py
├── phone
│ ├── __init__.py
│ ├── test_phone.py
│ └── test_is_valid.py
├── license_plate
│ ├── __init__.py
│ ├── test_license_plate.py
│ └── test_is_valid.py
├── test_cnh.py
├── test_renavam.py
├── test_email.py
├── test_pis.py
├── test_legal_nature.py
├── test_legal_process.py
├── test_cpf.py
├── test_cnpj.py
├── test_imports.py
├── test_voter_id.py
├── test_currency.py
├── test_date_utils.py
└── test_cep.py
├── brutils
├── ibge
│ ├── __init__.py
│ ├── uf.py
│ └── municipality.py
├── schemas
│ ├── __init__.py
│ └── address.py
├── exceptions
│ ├── __init__.py
│ └── cep.py
├── data
│ └── enums
│ │ ├── __init__.py
│ │ ├── better_enum.py
│ │ ├── uf.py
│ │ └── months.py
├── email.py
├── renavam.py
├── cnh.py
├── date_utils.py
├── pis.py
├── currency.py
├── __init__.py
├── legal_process.py
├── phone.py
├── legal_nature.py
├── cpf.py
├── license_plate.py
├── cnpj.py
└── cep.py
├── .flake8
├── codecov.yml
├── .github
├── FUNDING.yml
├── CODEOWNERS
├── workflows
│ ├── validate-pr-title-v1.yml
│ ├── autoassign-issue-v1.yml
│ ├── close-stale-issues-v1.yml
│ ├── publish-to-production.yml
│ ├── run-tests.yml
│ └── check-lint.yml
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── nova-funcionalidade-🌟.md
│ └── reportar-um-bug-🐛.md
└── pull_request_template.md
├── requirements-dev.txt
├── .githooks
└── pre-push
├── old_versions_documentation
├── README.md
└── v1.0.1
│ ├── ENGLISH_VERSION.md
│ └── PORTUGUESE_VERSION.md
├── Makefile
├── LICENSE
├── pyproject.toml
├── CORE_TEAM_EN.md
├── CORE_TEAM.md
├── CODE_OF_CONDUCT_EN.md
├── CODE_OF_CONDUCT.md
├── CHANGELOG.md
└── MAINTAINING.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/ibge/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/phone/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/brutils/ibge/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/license_plate/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length=80
3 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "tests"
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [cumbucadev, camilamaia]
2 |
--------------------------------------------------------------------------------
/brutils/schemas/__init__.py:
--------------------------------------------------------------------------------
1 | from .address import Address
2 |
--------------------------------------------------------------------------------
/brutils/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | from .cep import CEPNotFound, InvalidCEP
2 |
--------------------------------------------------------------------------------
/brutils/data/enums/__init__.py:
--------------------------------------------------------------------------------
1 | from brutils.data.enums.uf import UF, UF_CODE
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @brazilian-utils/python-maintainers @brazilian-utils/python-core-team
2 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | holidays>=0.58
2 | num2words==0.5.13
3 | coverage>=7.2.7
4 | ruff>=0.5.0,<0.7.0
5 |
--------------------------------------------------------------------------------
/brutils/exceptions/cep.py:
--------------------------------------------------------------------------------
1 | class InvalidCEP(Exception):
2 | def __init__(self, cep):
3 | self.cep = cep
4 | super().__init__(f"CEP '{cep}' is invalid.")
5 |
6 |
7 | class CEPNotFound(Exception):
8 | pass
9 |
--------------------------------------------------------------------------------
/.githooks/pre-push:
--------------------------------------------------------------------------------
1 | printf "Running pre-push hook checks\n\n"
2 |
3 | make check
4 |
5 | bash -c "if [[ $? != 0 ]];then printf '\nPlease run the following command before push:\n\n make format\n\n\
6 | Remember to commit any changes made on the process.\n'; exit 1; fi"
7 |
--------------------------------------------------------------------------------
/brutils/schemas/address.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict
2 |
3 |
4 | class Address(TypedDict):
5 | cep: str
6 | logradouro: str
7 | complemento: str
8 | bairro: str
9 | localidade: str
10 | uf: str
11 | ibge: str
12 | gia: str
13 | ddd: str
14 | siafi: str
15 |
--------------------------------------------------------------------------------
/.github/workflows/validate-pr-title-v1.yml:
--------------------------------------------------------------------------------
1 | name: "Validate PR title"
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - reopened
9 |
10 | permissions:
11 | pull-requests: write
12 |
13 | jobs:
14 | lint_pr:
15 | name: Validate
16 | uses: cumbucadev/shared-workflows/.github/workflows/validate-pr-title-v1.yml@main
17 |
--------------------------------------------------------------------------------
/brutils/data/enums/better_enum.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, EnumMeta
2 |
3 |
4 | class __MetaEnum(EnumMeta):
5 | @property
6 | def names(cls):
7 | return sorted(cls._member_names_)
8 |
9 | @property
10 | def values(cls):
11 | return sorted(list(map(lambda x: x.value, cls._member_map_.values())))
12 |
13 |
14 | class BetterEnum(Enum, metaclass=__MetaEnum):
15 | pass
16 |
--------------------------------------------------------------------------------
/.github/workflows/autoassign-issue-v1.yml:
--------------------------------------------------------------------------------
1 | name: Autoassign issue
2 |
3 | on:
4 | issue_comment:
5 | types:
6 | - created
7 |
8 | permissions:
9 | contents: read
10 | issues: write
11 | pull-requests: write
12 |
13 | jobs:
14 | issue_assign:
15 | name: "Call `Autoassign issue` shared workflow"
16 | uses: cumbucadev/shared-workflows/.github/workflows/autoassign-issue-v1.yml@main
17 |
--------------------------------------------------------------------------------
/.github/workflows/close-stale-issues-v1.yml:
--------------------------------------------------------------------------------
1 | name: "Close stale issues and PRs"
2 |
3 | on:
4 | schedule:
5 | - cron: '00 4 * * *'
6 | workflow_dispatch:
7 |
8 | permissions:
9 | actions: write
10 | issues: write
11 | pull-requests: write
12 |
13 | jobs:
14 | stale:
15 | name: Stale
16 | uses: cumbucadev/shared-workflows/.github/workflows/close-stale-issues-v1.yml@main
17 |
--------------------------------------------------------------------------------
/tests/test_cnh.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from brutils.cnh import is_valid_cnh
4 |
5 |
6 | class TestCNH(TestCase):
7 | def test_is_valid_cnh(self):
8 | self.assertFalse(is_valid_cnh("22222222222"))
9 | self.assertFalse(is_valid_cnh("ABC70304734"))
10 | self.assertFalse(is_valid_cnh("6619558737912"))
11 | self.assertTrue(is_valid_cnh("097703047-34"))
12 | self.assertTrue(is_valid_cnh("09770304734"))
13 |
--------------------------------------------------------------------------------
/old_versions_documentation/README.md:
--------------------------------------------------------------------------------
1 | # Documentação Antiga
2 |
3 | Aqui você vai encontrar a documentação de versões já descontinuadas.
4 |
5 | A documentação das versões que ainda estão sendo mantidas está disponível nos
6 | arquivos README.md/README_EN.md.
7 |
8 | # Old Documentation
9 |
10 | Here you will find documentation for discontinued versions.
11 |
12 | Documentation for versions that are still being maintained is available on
13 | README.md/README_EN.md.
14 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-production.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Production
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | build-n-publish:
7 | name: Publish to PyPI. Build and publish Python 🐍 distributions 📦
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout repo
11 | uses: actions/checkout@v4
12 | - name: Poetry Setup
13 | uses: snok/install-poetry@v1
14 | - name: Build and publish to pypi
15 | run: |
16 | poetry build
17 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}
18 | poetry publish
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip"
9 | directory: "/"
10 | schedule:
11 | interval: weekly
12 | time: "03:00"
13 | open-pull-requests-limit: 10
14 | allow:
15 | - dependency-type: direct
16 | - dependency-type: indirect
17 | - package-ecosystem: github-actions
18 | directory: /
19 | schedule:
20 | interval: weekly
21 | time: "03:00"
22 | open-pull-requests-limit: 10
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | @git config --local core.hooksPath .githooks/
3 | # This must be indented like this, otherwise it will not work on Windows
4 | # see: https://stackoverflow.com/questions/77974076/how-do-i-fix-this-error-when-checking-os-in-makefile
5 | ifneq ($(OS),Windows_NT)
6 | @chmod -R +x .githooks
7 | endif
8 | @poetry install
9 |
10 | shell:
11 | @poetry shell
12 |
13 | run-python:
14 | @poetry run python
15 |
16 | format:
17 | @poetry run ruff format .
18 | @poetry run ruff check --fix .
19 |
20 | check:
21 | @poetry run ruff format --check .
22 | @poetry run ruff check .
23 |
24 | test:
25 | ifeq ($(OS),Windows_NT)
26 | @set PYTHONDONTWRITEBYTECODE=1 && poetry run python -m unittest discover tests/ -v
27 | else
28 | @PYTHONDONTWRITEBYTECODE=1 poetry run python3 -m unittest discover tests/ -v
29 | endif
--------------------------------------------------------------------------------
/tests/test_renavam.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from brutils.renavam import is_valid_renavam
4 |
5 |
6 | class TestRENAVAM(TestCase):
7 | def test_is_valid_renavam(self):
8 | self.assertTrue(is_valid_renavam("86769597308"))
9 | self.assertFalse(is_valid_renavam("12345678901"))
10 | self.assertFalse(is_valid_renavam("1234567890a"))
11 | self.assertFalse(is_valid_renavam("12345678 901"))
12 | self.assertFalse(is_valid_renavam("12345678"))
13 | self.assertFalse(is_valid_renavam(""))
14 | self.assertFalse(is_valid_renavam("123456789012"))
15 | self.assertFalse(is_valid_renavam("abcdefghijk"))
16 | self.assertFalse(is_valid_renavam("12345678901!"))
17 | self.assertFalse(is_valid_renavam("00000000000"))
18 | self.assertFalse(is_valid_renavam("11111111111"))
19 |
--------------------------------------------------------------------------------
/brutils/email.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | def is_valid(email: str) -> bool:
5 | """
6 | Check if a string corresponds to a valid email address.
7 |
8 | Args:
9 | email (str): The input string to be checked.
10 |
11 | Returns:
12 | bool: True if email is a valid email address, False otherwise.
13 |
14 | Example:
15 | >>> is_valid("brutils@brutils.com")
16 | True
17 | >>> is_valid("invalid-email@brutils")
18 | False
19 |
20 | .. note::
21 | The rules for validating an email address generally follow the
22 | specifications defined by RFC 5322 (updated by RFC 5322bis),
23 | which is the widely accepted standard for email address formats.
24 | """
25 |
26 | pattern = re.compile(
27 | r"^(?![.])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
28 | )
29 | return isinstance(email, str) and re.match(pattern, email) is not None
30 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | python-version: ['3.10', '3.11', '3.12', '3.13']
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Set Up Python ${{ matrix.python-version }}
16 | uses: actions/setup-python@v5
17 | with:
18 | python-version: ${{ matrix.python-version }}
19 | - name: Poetry Setup
20 | uses: snok/install-poetry@v1
21 | with:
22 | version: 1.8.4
23 | - name: Install Dependencies
24 | run: poetry install
25 | - name: Generate Report
26 | run: |
27 | poetry run coverage run -m unittest discover -s tests
28 | - name: Upload Coverage to Codecov
29 | uses: codecov/codecov-action@v5
30 | with:
31 | token: ${{ secrets.CODECOV_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.github/workflows/check-lint.yml:
--------------------------------------------------------------------------------
1 | name: Check Lint
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | lint:
10 | name: Check Lint
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout repo
14 | uses: actions/checkout@v4
15 |
16 | - name: Poetry Setup
17 | uses: snok/install-poetry@v1
18 |
19 | - name: Install Dependencies
20 | run: poetry install
21 |
22 | - name: Run Lint Check
23 | id: lint
24 | run: |
25 | set -o pipefail
26 | make check 2>&1 | tee lint_output.log
27 | continue-on-error: true
28 |
29 | - name: Lint Failed
30 | if: steps.lint.outcome != 'success'
31 | run: |
32 | echo -e "\033[0;31m Linting failed. See the errors below:\n"
33 | cat lint_output.log
34 | echo -e "\n\033[0;33m Please, run \`make format\` and push the changes to fix this error."
35 | exit 1
36 |
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Brazilian Utils
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/brutils/data/enums/uf.py:
--------------------------------------------------------------------------------
1 | from brutils.data.enums.better_enum import BetterEnum
2 |
3 |
4 | class UF(BetterEnum):
5 | AC = "Acre"
6 | AL = "Alagoas"
7 | AP = "Amapá"
8 | AM = "Amazonas"
9 | BA = "Bahia"
10 | CE = "Ceará"
11 | DF = "Distrito Federal"
12 | ES = "Espírito Santo"
13 | GO = "Goiás"
14 | MA = "Maranhão"
15 | MT = "Mato Grosso"
16 | MS = "Mato Grosso do Sul"
17 | MG = "Minas Gerais"
18 | PA = "Pará"
19 | PB = "Paraíba"
20 | PR = "Paraná"
21 | PE = "Pernambuco"
22 | PI = "Piauí"
23 | RJ = "Rio de Janeiro"
24 | RN = "Rio Grande do Norte"
25 | RS = "Rio Grande do Sul"
26 | RO = "Rondônia"
27 | RR = "Roraima"
28 | SC = "Santa Catarina"
29 | SP = "São Paulo"
30 | SE = "Sergipe"
31 | TO = "Tocantins"
32 |
33 |
34 | class UF_CODE(BetterEnum):
35 | AC = "12"
36 | AL = "27"
37 | AP = "16"
38 | AM = "13"
39 | BA = "29"
40 | CE = "23"
41 | DF = "53"
42 | ES = "32"
43 | GO = "52"
44 | MA = "21"
45 | MT = "51"
46 | MS = "50"
47 | MG = "31"
48 | PA = "15"
49 | PB = "25"
50 | PR = "41"
51 | PE = "26"
52 | PI = "22"
53 | RJ = "33"
54 | RN = "24"
55 | RS = "43"
56 | RO = "11"
57 | RR = "14"
58 | SC = "42"
59 | SP = "35"
60 | SE = "28"
61 | TO = "17"
62 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "brutils"
3 | version = "2.3.0"
4 | description = "Utils library for specific Brazilian businesses"
5 | authors = ["The Brazilian Utils Organization"]
6 | license = "MIT"
7 | readme = "README.md"
8 | repository = "https://github.com/brazilian-utils/brutils"
9 | keywords = ["cpf", "cnpj", "cep", "document", "validation", "brazil", "brazilian"]
10 | classifiers = [
11 | "Development Status :: 5 - Production/Stable",
12 | "License :: OSI Approved :: MIT License",
13 | "Programming Language :: Python",
14 | "Programming Language :: Python :: 3 :: Only",
15 | "Programming Language :: Python :: 3.10",
16 | "Programming Language :: Python :: 3.11",
17 | "Programming Language :: Python :: 3.12",
18 | "Programming Language :: Python :: 3.13",
19 | "Topic :: Office/Business",
20 | "Topic :: Software Development :: Internationalization",
21 | "Topic :: Software Development :: Libraries :: Python Modules",
22 | "Natural Language :: English",
23 | "Natural Language :: Portuguese",
24 | "Natural Language :: Portuguese (Brazilian)"
25 | ]
26 |
27 | [tool.poetry.dependencies]
28 | python = "^3.10"
29 | holidays = "^0.58"
30 | num2words = "0.5.14"
31 | coverage = "^7.2.7"
32 |
33 | [tool.poetry.group.test.dependencies]
34 | coverage = "^7.2.7"
35 |
36 | [tool.poetry.group.dev.dependencies]
37 | ruff = ">=0.5.0,<0.7.0"
38 |
39 | [tool.ruff]
40 | line-length = 80
41 | lint.extend-select = ["I"]
42 |
43 | [tool.ruff.lint.per-file-ignores]
44 | "__init__.py" = ["F401"]
45 |
46 | [build-system]
47 | requires = ["poetry-core"]
48 | build-backend = "poetry.core.masonry.api"
49 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/nova-funcionalidade-🌟.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Nova Funcionalidade \U0001F31F"
3 | about: Sugestão de uma nova funcionalidade
4 | title: ''
5 | labels: feature
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Seu pedido de recurso está relacionado a um problema? Por favor, descreva.**
11 | Uma descrição clara e concisa do que é o problema. Por exemplo: "Fico sempre frustrado quando [...]"
12 |
13 | **Descreva a solução que você gostaria**
14 | Uma descrição clara e concisa do que você deseja que aconteça.
15 |
16 | **Descreva alternativas que você considerou**
17 | Uma descrição clara e concisa de quaisquer soluções ou recursos alternativos que você tenha considerado.
18 |
19 | **Contexto adicional**
20 | Adicione qualquer outro contexto ou capturas de tela sobre o pedido de recurso aqui.
21 |
22 | **💌 Quer contribuir, mas não se sente à vontade?**
23 |
24 | Você tem vontade de contribuir, mas não se sente à vontade em abrir issues, PRs ou fazer perguntas publicamente?
25 |
26 | Nós sabemos como pode ser difícil dar o primeiro passo em um espaço aberto. A insegurança, o medo de errar ou até a sensação de “será que minha dúvida é boba?” podem pesar bastante. E tá tudo bem sentir isso. 💜
27 |
28 | Queremos que você saiba que aqui ninguém precisa enfrentar esse caminho sem apoio. Se preferir um espaço mais reservado, você pode mandar um e-mail para cumbucadev@gmail.com e teremos o maior prazer em ajudar. Seja para tirar dúvidas, pedir orientação ou simplesmente ter alguém para conversar sobre como começar.
29 |
30 | O importante é que você saiba: sua participação é muito bem-vinda, e cada contribuição, por menor que pareça, faz uma grande diferença. ✨
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/reportar-um-bug-🐛.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Reportar um bug \U0001F41B"
3 | about: Reporte um bug, nos ajude a melhorar!
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Descrição do problema**
11 | Uma descrição clara e concisa do que é o erro.
12 |
13 | **Para Reproduzir**
14 | Passos para reproduzir o comportamento:
15 | 1. Chamar o utilitário '...'
16 | 2. Passando o parâmetro '....'
17 | 3. Ver o erro
18 |
19 | **Comportamento esperado**
20 | Uma descrição clara e concisa do que você esperava que acontecesse.
21 |
22 | **Desktop (por favor, forneça as seguintes informações):**
23 | - Sistema Operacional: [por exemplo, iOS]
24 | - Versão do brutils: [por exemplo, 2.0.0]
25 |
26 | **Contexto adicional**
27 | Adicione qualquer outro contexto sobre o problema aqui.
28 |
29 | **💌 Quer contribuir, mas não se sente à vontade?**
30 |
31 | Você tem vontade de contribuir, mas não se sente à vontade em abrir issues, PRs ou fazer perguntas publicamente?
32 |
33 | Nós sabemos como pode ser difícil dar o primeiro passo em um espaço aberto. A insegurança, o medo de errar ou até a sensação de “será que minha dúvida é boba?” podem pesar bastante. E tá tudo bem sentir isso. 💜
34 |
35 | Queremos que você saiba que aqui ninguém precisa enfrentar esse caminho sem apoio. Se preferir um espaço mais reservado, você pode mandar um e-mail para cumbucadev@gmail.com e teremos o maior prazer em ajudar. Seja para tirar dúvidas, pedir orientação ou simplesmente ter alguém para conversar sobre como começar.
36 |
37 | O importante é que você saiba: sua participação é muito bem-vinda, e cada contribuição, por menor que pareça, faz uma grande diferença. ✨
38 |
--------------------------------------------------------------------------------
/brutils/data/enums/months.py:
--------------------------------------------------------------------------------
1 | from brutils.data.enums.better_enum import BetterEnum
2 |
3 |
4 | class MonthsEnum(BetterEnum):
5 | JANEIRO = 1
6 | FEVEREIRO = 2
7 | MARCO = 3
8 | ABRIL = 4
9 | MAIO = 5
10 | JUNHO = 6
11 | JULHO = 7
12 | AGOSTO = 8
13 | SETEMBRO = 9
14 | OUTUBRO = 10
15 | NOVEMBRO = 11
16 | DEZEMBRO = 12
17 |
18 | @property
19 | def month_name(self) -> str:
20 | if self == MonthsEnum.JANEIRO:
21 | return "janeiro"
22 | elif self == MonthsEnum.FEVEREIRO:
23 | return "fevereiro"
24 | elif self == MonthsEnum.MARCO:
25 | return "marco"
26 | elif self == MonthsEnum.ABRIL:
27 | return "abril"
28 | elif self == MonthsEnum.MAIO:
29 | return "maio"
30 | elif self == MonthsEnum.JUNHO:
31 | return "junho"
32 | elif self == MonthsEnum.JULHO:
33 | return "julho"
34 | elif self == MonthsEnum.AGOSTO:
35 | return "agosto"
36 | elif self == MonthsEnum.SETEMBRO:
37 | return "setembro"
38 | elif self == MonthsEnum.OUTUBRO:
39 | return "outubro"
40 | elif self == MonthsEnum.NOVEMBRO:
41 | return "novembro"
42 | else:
43 | return "dezembro"
44 |
45 | @classmethod
46 | def is_valid_month(cls, month: int) -> bool:
47 | """
48 | Checks if the given month value is valid.
49 | Args:
50 | month (int): The month to check.
51 |
52 | Returns:
53 | True if the month is valid, False otherwise.
54 | """
55 | return (
56 | True if month in set(month.value for month in MonthsEnum) else False
57 | )
58 |
--------------------------------------------------------------------------------
/brutils/renavam.py:
--------------------------------------------------------------------------------
1 | RENAVAM_DV_WEIGHTS = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3]
2 |
3 |
4 | def _validate_renavam_format(renavam: str):
5 | if not isinstance(renavam, str):
6 | return False
7 | if len(renavam) != 11 or not renavam.isdigit():
8 | return False
9 | if len(set(renavam)) == 1:
10 | return False
11 | return True
12 |
13 |
14 | def _sum_weighted_digits(renavam: str) -> int:
15 | base_digits = [int(d) for d in renavam[:-1][::-1]]
16 | return sum(x * y for x, y in zip(base_digits, RENAVAM_DV_WEIGHTS))
17 |
18 |
19 | def _calculate_renavam_dv(renavam: str):
20 | weighted_sum = _sum_weighted_digits(renavam)
21 | dv = 11 - (weighted_sum % 11)
22 | return 0 if dv >= 10 else dv
23 |
24 |
25 | def is_valid_renavam(renavam: str) -> bool:
26 | """
27 | Validates the Brazilian vehicle registration number (RENAVAM).
28 |
29 | This function takes a RENAVAM string and checks if it is valid.
30 | A valid RENAVAM consists of exactly 11 digits, with the last digit as
31 | a verification digit calculated from the previous 10 digits.
32 |
33 | Args:
34 | renavam (str): The RENAVAM string to be validated.
35 |
36 | Returns:
37 | bool: True if the RENAVAM is valid, False otherwise.
38 |
39 | Example:
40 | >>> is_valid_renavam('86769597308')
41 | True
42 | >>> is_valid_renavam('12345678901')
43 | False
44 | >>> is_valid_renavam('1234567890a')
45 | False
46 | >>> is_valid_renavam('12345678 901')
47 | False
48 | >>> is_valid_renavam('12345678')
49 | False
50 | >>> is_valid_renavam('')
51 | False
52 | """
53 | if not _validate_renavam_format(renavam):
54 | return False
55 |
56 | return _calculate_renavam_dv(renavam) == int(renavam[-1])
57 |
--------------------------------------------------------------------------------
/old_versions_documentation/v1.0.1/ENGLISH_VERSION.md:
--------------------------------------------------------------------------------
1 | # brutils
2 |
3 | ### [Procurando pela versão em português?](PORTUGUESE_VERSION.md)
4 |
5 | _Compatible with Python 2.7 and 3.x_
6 |
7 | `brutils` is a library for validating brazilian document numbers, and might
8 | eventually evolve to deal with other validations related to brazilian bureaucracy.
9 |
10 | It's main functionality is the validation of CPF and CNPJ numbers, but suggestions
11 | for other (preferrably deterministic) things to validate are welcome.
12 |
13 |
14 | ## Installation
15 |
16 | ```
17 | pip install brutils
18 | ```
19 |
20 |
21 | ## Utilization
22 |
23 | ### Importing:
24 | ```
25 | >>> from brutils import cpf, cnpj
26 | ```
27 |
28 | ### How do I validate a CPF or CNPJ?
29 | ```
30 | # numbers only, formatted as strings
31 |
32 | >>> cpf.validate('00011122233')
33 | False
34 | >>> cnpj.validate('00111222000133')
35 | False
36 | ```
37 |
38 | ### What if my string has formatting symbols in it?
39 | ```
40 | >>> cpf.sieve('000.111.222-33')
41 | '00011122233'
42 | >>> cnpj.sieve('00.111.222/0001-00')
43 | '00111222000100'
44 |
45 | # The `sieve` function only filters out the symbols used for CPF or CNPJ validation.
46 | # It purposefully doesn't remove other symbols, as those may be indicators of data
47 | # corruption, or a possible lack of input filters.
48 | ```
49 |
50 | ### What if I want to format a numbers only string?
51 | ```
52 | >>> cpf.display('00011122233')
53 | '000.111.222-33'
54 | >>> cnpj.display('00111222000100')
55 | '00.111.222/0001-00'
56 | ```
57 |
58 | ### What if I want to generate random, but numerically valid CPF or CNPJ numbers?
59 | ```
60 | >>> cpf.generate()
61 | '17433964657'
62 | >>> cnpj.generate()
63 | '34665388000161'
64 | ```
65 |
66 |
67 | ## Testing
68 |
69 | ```
70 | python2.7 -m unittest discover tests/
71 | python3 -m unittest discover tests/
72 | ```
73 |
--------------------------------------------------------------------------------
/tests/test_email.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from brutils import is_valid_email
4 |
5 |
6 | class TestEmailValidation(TestCase):
7 | def test_valid_email(self):
8 | # Valid email addresses
9 | valid_emails = [
10 | "joao.ninguem@gmail.com",
11 | "user123@gmail.com",
12 | "test.email@mydomain.co.uk",
13 | "johndoe@sub.domain.example",
14 | "f99999999@place.university-campus.ac.in",
15 | ]
16 | for email in valid_emails:
17 | try:
18 | self.assertTrue(is_valid_email(email))
19 | except: # noqa: E722
20 | print(f"AssertionError for email: {email}")
21 | raise AssertionError
22 |
23 | def test_invalid_email(self):
24 | # Invalid email addresses
25 | invalid_emails = [
26 | ".joao.ninguem@gmail.com",
27 | "joao ninguem@gmail.com",
28 | "not_an_email",
29 | "@missing_username.com",
30 | "user@incomplete.",
31 | "user@.incomplete",
32 | "user@inva!id.com",
33 | "user@missing-tld.",
34 | ]
35 | for email in invalid_emails:
36 | try:
37 | self.assertFalse(is_valid_email(email))
38 | except: # noqa: E722
39 | print(f"AssertionError for email: {email}")
40 | raise AssertionError
41 |
42 | def test_non_string_input(self):
43 | # Non-string input should return False
44 | non_strings = [None, 123, True, ["test@example.com"]]
45 | for value in non_strings:
46 | self.assertFalse(is_valid_email(value))
47 |
48 | def test_empty_string(self):
49 | # Empty string should return False
50 | self.assertFalse(is_valid_email(""))
51 |
52 |
53 | if __name__ == "__main__":
54 | main()
55 |
--------------------------------------------------------------------------------
/old_versions_documentation/v1.0.1/PORTUGUESE_VERSION.md:
--------------------------------------------------------------------------------
1 | # brutils
2 |
3 | ### [Looking for the english version?](ENGLISH_VERSION.md)
4 |
5 | _Compatível com Python 2.7 e 3.x_
6 |
7 | `brutils` é uma biblioteca para tratar de validações de documentos brasileiros,
8 | e que eventualmente pode evoluir para tratar de outras coisas dentro do escopo
9 | de validações relacionadas a burocracias brasileiras.
10 |
11 | Sua principal funcionalidade é a validação de CPFs e CNPJs, mas sugestões sobre
12 | outras coisas a se validar (preferencialmente de maneira determinística) são bem
13 | vindas.
14 |
15 |
16 | ## Instalação
17 |
18 | ```
19 | pip install brutils
20 | ```
21 |
22 |
23 | ## Utilização
24 |
25 | ### Importando a Biblioteca:
26 | ```
27 | >>> from brutils import cpf, cnpj
28 | ```
29 |
30 | ### Como faço para validar um CPF ou CNPJ?
31 | ```
32 | # somente numeros, em formato string
33 |
34 | >>> cpf.validate('00011122233')
35 | False
36 | >>> cnpj.validate('00111222000133')
37 | False
38 | ```
39 |
40 | ### E se a minha string estiver formatada com simbolos?
41 | ```
42 | >>> cpf.sieve('000.111.222-33')
43 | '00011122233'
44 | >>> cnpj.sieve('00.111.222/0001-00')
45 | '00111222000100'
46 |
47 | # A função `sieve` limpa apenas os simbolos de formatação de CPF ou CNPJ, e de
48 | # whitespace nas pontas. Ela não remove outros caractéres propositalmente, pois
49 | # estes seriam indicativos de uma possível corrupção no dado ou de uma falta de
50 | # filtros de input.
51 | ```
52 |
53 | ### E se eu quiser formatar uma string numérica?
54 | ```
55 | >>> cpf.display('00011122233')
56 | '000.111.222-33'
57 | >>> cnpj.display('00111222000100')
58 | '00.111.222/0001-00'
59 | ```
60 |
61 | ### E se eu quiser gerar CPFs ou CNPJs validos aleatórios?
62 | ```
63 | >>> cpf.generate()
64 | '17433964657'
65 | >>> cnpj.generate()
66 | '34665388000161'
67 | ```
68 |
69 |
70 | ## Testes
71 |
72 | ```
73 | python2.7 -m unittest discover tests/
74 | python3 -m unittest discover tests/
75 | ```
76 |
--------------------------------------------------------------------------------
/CORE_TEAM_EN.md:
--------------------------------------------------------------------------------
1 |
2 |
🇧🇷 Core Team brutils-python
3 |
4 |
5 | The _Core Team_ of `brazilian-utils/brutils-python` is comprised of a group of contributors who have demonstrated remarkable enthusiasm for the project and the community. This team holds administrative privileges on GitHub specific to this repository.
6 |
7 | ## Responsibilities
8 |
9 | The _Core Team_ of `brutils-python` has the following responsibilities:
10 |
11 | * Be available to address strategic inquiries regarding the vision and future prospects of Brazilian Utils Python.
12 |
13 | * Commit to reviewing pull requests that have been submitted for some time or have been overlooked.
14 |
15 | * Periodically inspect open issues in Brazilian Utils Python, provide constructive suggestions, and categorize them using specific labels on GitHub.
16 |
17 | * Identify promising individuals in the Brazilian Utils Python community who may express interest in joining the team and contributing significantly.
18 |
19 | Similar to all contributors at `brutils-python`, members of the Core Team also operate as volunteers in the open-source realm; being part of the team is not an obligation. This team is recognized as leaders in this community, and while they are a reliable reference for obtaining answers to questions, it's important to note that they contribute voluntarily, dedicating their time, which may result in non-immediate availability.
20 |
21 | ## Members
22 |
23 | - [@camilamaia](https://github.com/camilamaia)
24 | - [@antoniamaia](https://github.com/antoniamaia)
25 |
26 | ## Adding New Members
27 |
28 | The process for incorporating new members into the _Core Team_ `brutils-python` is as follows:
29 |
30 | * An existing team member privately contacts the individual to gauge their interest. If there is interest, a pull request is opened, adding the new member to the list.
31 |
32 | * Other team members review the pull request. The person responsible for merging the PR is also tasked with adding the new member to the Core Team group on GitHub.
33 |
--------------------------------------------------------------------------------
/CORE_TEAM.md:
--------------------------------------------------------------------------------
1 |
2 |
🇧🇷 Core Team brutils-python
3 |
4 |
5 | O _Core Team_ do `brazilian-utils/brutils-python` é composto por um conjunto de colaboradores que manifestaram um entusiasmo notável pelo projeto e pela comunidade. Essa equipe possui privilégios administrativos no GitHub específicos para o repositório.
6 |
7 | ## Responsabilidades
8 |
9 | O _Core Team_ do `brutils-python` possui as seguintes responsabilidades:
10 |
11 | * Estar prontamente disponível para abordar questionamentos de natureza estratégica acerca da visão e perspectivas futuras do Brazilian Utils Python.
12 |
13 | * Comprometer-se a revisar pull requests que tenham sido submetidos há algum tempo ou que tenham sido negligenciados.
14 |
15 | * Periodicamente examinar as questões abertas no Brazilian Utils Python, fornecer sugestões construtivas e categorizá-las utilizando rótulos específicos no GitHub.
16 |
17 | * Identificar indivíduos promissores na comunidade do Brazilian Utils Python que possam expressar interesse em integrar a equipe e contribuir de maneira significativa.
18 |
19 | Da mesma forma que todos os colaboradores do `brutils-python`, os membros do Core Team também atuam como voluntários no âmbito de código aberto; fazer parte da equipe não é uma obrigação. Essa equipe é reconhecida como líder nesta comunidade e, embora seja uma referência confiável para obter respostas a perguntas, é importante salientar que eles contribuem de forma voluntária, dedicando seu tempo, o que pode resultar em disponibilidade não imediata.
20 |
21 |
22 | ## Membros
23 |
24 | - [@camilamaia](https://github.com/camilamaia)
25 | - [@antoniamaia](https://github.com/antoniamaia)
26 |
27 |
28 | ## Adição de Novos Membros
29 |
30 | O procedimento para incorporar novos membros ao _Core Team_ `brutils-python` é o seguinte:
31 |
32 | * Um integrante já existente da equipe entra em contato de forma privada para averiguar o interesse da pessoa. Se houver interesse, é aberto um pull request adicionando o novo membro à lista.
33 |
34 | * Os outros integrantes da equipe revisam o pull request. A pessoa responsável por efetuar o merge no PR é também encarregada de incluir o novo membro no grupo Core Team no GitHub.
35 |
36 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Descrição
2 |
3 |
4 | ## Mudanças Propostas
5 |
10 |
11 | ## Checklist de Revisão
12 |
13 |
14 | - [ ] Eu li o [Contributing.md](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md)
15 | - [ ] Os testes foram adicionados ou atualizados para refletir as mudanças (se aplicável).
16 | - [ ] Foi adicionada uma entrada no changelog / Meu PR não necessita de uma nova entrada no changelog.
17 | - [ ] A [documentação](https://github.com/brazilian-utils/brutils-python/blob/main/README.md) em português foi atualizada ou criada, se necessário.
18 | - [ ] Se feita a documentação, a atualização do [arquivo em inglês](https://github.com/brazilian-utils/brutils-python/blob/main/README_EN.md).
19 | - [ ] Eu documentei as minhas mudanças no código, adicionando docstrings e comentários. [Instruções](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#8-fa%C3%A7a-as-suas-altera%C3%A7%C3%B5es)
20 | - [ ] O código segue as diretrizes de estilo e padrões de codificação do projeto.
21 | - [ ] Todos os testes passam. [Instruções](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#testes)
22 | - [ ] O Pull Request foi testado localmente. [Instruções](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#7-execute-o-brutils-localmente)
23 | - [ ] Não há conflitos de mesclagem.
24 |
25 | ## Comentários Adicionais (opcional)
26 |
27 |
28 | ## Issue Relacionada
29 |
30 |
31 | Closes #
32 |
--------------------------------------------------------------------------------
/brutils/cnh.py:
--------------------------------------------------------------------------------
1 | def is_valid_cnh(cnh: str) -> bool:
2 | """
3 | Validates the registration number for the Brazilian CNH (Carteira Nacional de Habilitação) that was created in 2022.
4 | Previous versions of the CNH are not supported in this version.
5 | This function checks if the given CNH is valid based on the format and allowed characters,
6 | verifying the verification digits.
7 |
8 | Args:
9 | cnh (str): CNH string (symbols will be ignored).
10 |
11 | Returns:
12 | bool: True if CNH has a valid format.
13 |
14 | Examples:
15 | >>> is_valid_cnh("12345678901")
16 | False
17 | >>> is_valid_cnh("A2C45678901")
18 | False
19 | >>> is_valid_cnh("98765432100")
20 | True
21 | >>> is_valid_cnh("987654321-00")
22 | True
23 | """
24 | cnh = "".join(
25 | filter(str.isdigit, cnh)
26 | ) # clean the input and check for numbers only
27 |
28 | if not cnh:
29 | return False
30 |
31 | if len(cnh) != 11:
32 | return False
33 |
34 | # Reject sequences as "00000000000", "11111111111", etc.
35 | if cnh == cnh[0] * 11:
36 | return False
37 |
38 | # cast digits to list of integers
39 | digits: list[int] = [int(ch) for ch in cnh]
40 | first_verificator = digits[9]
41 | second_verificator = digits[10]
42 |
43 | if not _check_first_verificator(
44 | digits, first_verificator
45 | ): # checking the 10th digit
46 | return False
47 |
48 | return _check_second_verificator(
49 | digits, second_verificator, first_verificator
50 | ) # checking the 11th digit
51 |
52 |
53 | def _check_first_verificator(digits: list[int], first_verificator: int) -> bool:
54 | """
55 | Generates the first verification digit and uses it to verify the 10th digit of the CNH
56 | """
57 |
58 | sum = 0
59 | for i in range(9):
60 | sum += digits[i] * (9 - i)
61 |
62 | sum = sum % 11
63 | result = 0 if sum > 9 else sum
64 |
65 | return result == first_verificator
66 |
67 |
68 | def _check_second_verificator(
69 | digits: list[int], second_verificator: int, first_verificator: int
70 | ) -> bool:
71 | """
72 | Generates the second verification and uses it to verify the 11th digit of the CNH
73 | """
74 | sum = 0
75 | for i in range(9):
76 | sum += digits[i] * (i + 1)
77 |
78 | result = sum % 11
79 |
80 | if first_verificator > 9:
81 | result = result + 9 if (result - 2) < 0 else result - 2
82 |
83 | if result > 9:
84 | result = 0
85 |
86 | return result == second_verificator
87 |
--------------------------------------------------------------------------------
/tests/test_pis.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 | from unittest.mock import patch
3 |
4 | from brutils.pis import (
5 | _checksum,
6 | format_pis,
7 | generate,
8 | is_valid,
9 | remove_symbols,
10 | )
11 |
12 |
13 | class TestPIS(TestCase):
14 | def test_is_valid(self):
15 | # When PIS is not string, returns False
16 | self.assertIs(is_valid(1), False)
17 | self.assertIs(is_valid([]), False)
18 | self.assertIs(is_valid({}), False)
19 | self.assertIs(is_valid(None), False)
20 |
21 | # When PIS's len is different of 11, returns False
22 | self.assertIs(is_valid("123456789"), False)
23 |
24 | # When PIS does not contain only digits, returns False
25 | self.assertIs(is_valid("123pis"), False)
26 | self.assertIs(is_valid("123456789ab"), False)
27 |
28 | # When checksum digit doesn't match last digit, returns False
29 | self.assertIs(is_valid("11111111111"), False)
30 | self.assertIs(is_valid("11111111215"), False)
31 | self.assertIs(is_valid("12038619493"), False)
32 |
33 | # When PIS is valid
34 | self.assertIs(is_valid("12038619494"), True)
35 | self.assertIs(is_valid("12016784018"), True)
36 | self.assertIs(is_valid("12083210826"), True)
37 |
38 | def test_checksum(self):
39 | # Checksum digit is 0 when the subtracted number is 10 or 11
40 | self.assertEqual(_checksum("1204152015"), 0)
41 | self.assertEqual(_checksum("1204433157"), 0)
42 |
43 | # Checksum digit is equal the subtracted number
44 | self.assertEqual(_checksum("1204917738"), 2)
45 | self.assertEqual(_checksum("1203861949"), 4)
46 | self.assertEqual(_checksum("1208321082"), 6)
47 |
48 | def test_generate(self):
49 | for _ in range(10_000):
50 | self.assertIs(is_valid(generate()), True)
51 |
52 | def test_remove_symbols(self):
53 | self.assertEqual(remove_symbols("00000000000"), "00000000000")
54 | self.assertEqual(remove_symbols("170.33259.50-4"), "17033259504")
55 | self.assertEqual(remove_symbols("134..2435/.-1892.-"), "1342435/1892")
56 | self.assertEqual(remove_symbols("abc1230916*!*"), "abc1230916*!*")
57 | self.assertEqual(remove_symbols("...---..."), "")
58 |
59 | @patch("brutils.pis.is_valid")
60 | def test_format_valid_pis(self, mock_is_valid):
61 | mock_is_valid.return_value = True
62 |
63 | # When PIS is_valid, returns formatted PIS
64 | self.assertEqual(format_pis("14372195539"), "143.72195.53-9")
65 |
66 | # Checks if function is_valid_pis is called
67 | mock_is_valid.assert_called_once_with("14372195539")
68 |
69 | @patch("brutils.pis.is_valid")
70 | def test_format_invalid_pis(self, mock_is_valid):
71 | mock_is_valid.return_value = False
72 |
73 | # When PIS isn't valid, returns None
74 | self.assertIsNone(format_pis("14372195539"))
75 |
76 |
77 | if __name__ == "__main__":
78 | main()
79 |
--------------------------------------------------------------------------------
/tests/test_legal_nature.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from brutils.legal_nature import (
4 | LEGAL_NATURE,
5 | get_description,
6 | is_valid,
7 | list_all,
8 | )
9 |
10 |
11 | class TestNaturezaJuridica(TestCase):
12 | def test_is_valid_non_string(self):
13 | # When input is not string, returns False
14 | self.assertIs(is_valid(1234), False)
15 | self.assertIs(is_valid(None), False)
16 | self.assertIs(is_valid([]), False)
17 | self.assertIs(is_valid({}), False)
18 |
19 | def test_is_valid_invalid_patterns(self):
20 | # Wrong length after normalization or contains no digits
21 | self.assertIs(is_valid(""), False)
22 | self.assertIs(is_valid("20"), False)
23 | self.assertIs(is_valid("20623"), False)
24 | self.assertIs(is_valid("abcd"), False)
25 | self.assertIs(is_valid("---"), False)
26 |
27 | def test_is_valid_formats_hyphen_and_plain(self):
28 | # Accept both "NNNN" and "NNN-N" formats
29 | self.assertTrue(is_valid("2062"))
30 | self.assertTrue(is_valid("206-2"))
31 | self.assertTrue(is_valid("101-5"))
32 | self.assertTrue(is_valid("1015"))
33 |
34 | def test_is_valid_known_codes_true(self):
35 | # A few known valid codes from different sections
36 | for code in (
37 | "1015",
38 | "2062",
39 | "2143",
40 | "2305",
41 | "3034",
42 | "3131",
43 | "3212",
44 | "4014",
45 | "5002",
46 | ):
47 | self.assertTrue(is_valid(code))
48 |
49 | def test_get_description_known(self):
50 | self.assertEqual(
51 | get_description("2062"), "Sociedade Empresária Limitada"
52 | )
53 | self.assertEqual(
54 | get_description("101-5"), "Órgão Público do Poder Executivo Federal"
55 | )
56 | self.assertEqual(get_description("2143"), "Cooperativa")
57 | self.assertEqual(
58 | get_description("5002"),
59 | "Organização Internacional e Outras Instituições Extraterritoriais",
60 | )
61 |
62 | def test_get_description_invalid(self):
63 | self.assertIsNone(get_description("9999"))
64 | self.assertIsNone(get_description("0000"))
65 | self.assertIsNone(get_description("20A2"))
66 | self.assertIsNone(get_description(None)) # type: ignore[arg-type]
67 |
68 | def test_table_integrity(self):
69 | # Dictionary should contain only 4-digit string keys and string descriptions
70 | for k, v in LEGAL_NATURE.items():
71 | self.assertIsInstance(k, str)
72 | self.assertTrue(
73 | k.isdigit() and len(k) == 4, msg=f"Invalid key: {k}"
74 | )
75 | self.assertIsInstance(v, str)
76 | self.assertTrue(len(v) > 0)
77 |
78 | def test_list_all_returns_copy(self):
79 | data = list_all()
80 | self.assertEqual(data["2062"], "Sociedade Empresária Limitada")
81 | data["2062"] = "X"
82 | self.assertEqual(LEGAL_NATURE["2062"], "Sociedade Empresária Limitada")
83 |
84 |
85 | if __name__ == "__main__":
86 | main()
87 |
--------------------------------------------------------------------------------
/tests/test_legal_process.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from unittest import TestCase, main
3 |
4 | from brutils.legal_process import (
5 | _checksum,
6 | format_legal_process,
7 | generate,
8 | is_valid,
9 | remove_symbols,
10 | )
11 |
12 |
13 | class TestLegalProcess(TestCase):
14 | def test_format_legal_process(self):
15 | self.assertEqual(
16 | format_legal_process("23141945820055070079"),
17 | ("2314194-58.2005.5.07.0079"),
18 | )
19 | self.assertEqual(
20 | format_legal_process("00000000000000000000"),
21 | ("0000000-00.0000.0.00.0000"),
22 | )
23 | self.assertIsInstance(
24 | format_legal_process("00000000000000000000"),
25 | str,
26 | )
27 | self.assertIsNone(format_legal_process("2314194582005507"))
28 | self.assertIsNone(format_legal_process("0000000000000000000000000"))
29 | self.assertIsNone(format_legal_process("0000000000000000000asdasd"))
30 |
31 | def test_remove_symbols(self):
32 | self.assertEqual(
33 | remove_symbols("6439067-89.2023.4.04.5902"), "64390678920234045902"
34 | )
35 | self.assertEqual(
36 | remove_symbols("4976023-82.2012.7.00.2263"), "49760238220127002263"
37 | )
38 | self.assertEqual(
39 | remove_symbols("4976...-02382-.-2012.-7002--263"),
40 | "49760238220127002263",
41 | )
42 | self.assertEqual(
43 | remove_symbols("4976023-82.2012.7.00.2263*!*"),
44 | "49760238220127002263*!*",
45 | )
46 | self.assertEqual(
47 | remove_symbols("4976..#.-0@2382-.#-2012.#-7002--263@"),
48 | "4976#0@2382#2012#7002263@",
49 | )
50 | self.assertEqual(remove_symbols("@...---...#"), "@#")
51 | self.assertEqual(remove_symbols("...---..."), "")
52 | self.assertIsInstance(remove_symbols("...---..."), str)
53 |
54 | def test_generate(self):
55 | self.assertEqual(generate()[9:13], str(datetime.now().year))
56 | self.assertEqual(generate(year=3000)[9:13], "3000")
57 | self.assertEqual(generate(orgao=4)[13:14], "4")
58 | self.assertEqual(generate(year=3000, orgao=4)[9:13], "3000")
59 | self.assertIsInstance(generate(year=3000, orgao=4)[9:13], str)
60 | self.assertIsNone(generate(year=1000, orgao=4))
61 | self.assertIsNone(generate(orgao=0))
62 |
63 | def test_check_sum(self):
64 | self.assertEqual(_checksum(546611720238150014), "77")
65 | self.assertEqual(_checksum(403818720238230498), "50")
66 | self.assertIsInstance(_checksum(403818720238230498), str)
67 |
68 | def test_is_valid(self):
69 | self.assertIs(is_valid("10188748220234018200"), True)
70 | self.assertIs(is_valid("45532346920234025107"), True)
71 | self.assertIs(is_valid("10188748220239918200"), False)
72 | self.assertIs(is_valid("00000000000000000000"), False)
73 | self.assertIs(is_valid("455323469202340251"), False)
74 | self.assertIs(is_valid("455323469202340257123123123"), False)
75 | self.assertIs(is_valid("455323423QQWEQWSsasd&*(()"), False)
76 | self.assertIsInstance(is_valid("455323423QQWEQWSsasd&*(()"), bool)
77 |
78 |
79 | if __name__ == "__main__":
80 | main()
81 |
--------------------------------------------------------------------------------
/brutils/date_utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | from datetime import datetime
3 |
4 | import holidays
5 | from num2words import num2words
6 |
7 | from brutils.data.enums.months import MonthsEnum
8 |
9 | DATE_REGEX = re.compile(r"^\d{2}/\d{2}/\d{4}$")
10 |
11 |
12 | def is_holiday(target_date: datetime, uf: str = None) -> bool | None:
13 | """
14 | Checks if the given date is a national or state holiday in Brazil.
15 |
16 | This function takes a date as a `datetime` object and an optional UF (Unidade Federativa),
17 | returning a boolean value indicating whether the date is a holiday or `None` if the date or
18 | UF are invalid.
19 |
20 | The method does not handle municipal holidays.
21 |
22 | Args:
23 | target_date (datetime): The date to be checked.
24 | uf (str, optional): The state abbreviation (UF) to check for state holidays.
25 | If not provided, only national holidays will be considered.
26 |
27 | Returns:
28 | bool | None: Returns `True` if the date is a holiday, `False` if it is not,
29 | or `None` if the date or UF are invalid.
30 |
31 | Note:
32 | The function logic should be implemented using the `holidays` library.
33 | For more information, refer to the documentation at: https://pypi.org/project/holidays/
34 |
35 | Usage Examples:
36 | >>> from datetime import datetime
37 | >>> is_holiday(datetime(2024, 1, 1))
38 | True
39 |
40 | >>> is_holiday(datetime(2024, 1, 2))
41 | False
42 |
43 | >>> is_holiday(datetime(2024, 3, 2), uf="SP")
44 | False
45 |
46 | >>> is_holiday(datetime(2024, 12, 25), uf="RJ")
47 | True
48 | """
49 | if not isinstance(target_date, datetime):
50 | return None
51 |
52 | national_holidays = holidays.Brazil(years=target_date.year)
53 | valid_ufs = national_holidays.subdivisions
54 |
55 | if uf is not None and uf not in valid_ufs:
56 | return None
57 |
58 | if uf is None:
59 | return target_date in national_holidays
60 |
61 | state_holidays = holidays.Brazil(prov=uf, years=target_date.year)
62 | return target_date in state_holidays
63 |
64 |
65 | def convert_date_to_text(date: str) -> str | None:
66 | """
67 | Converts a given date in Brazilian format (dd/mm/yyyy) to its textual representation.
68 |
69 | This function takes a date as a string in the format dd/mm/yyyy and converts it
70 | to a string with the date written out in Brazilian Portuguese, including the full
71 | month name and the year.
72 |
73 | Args:
74 | date (str): The date to be converted into text. Expected format: dd/mm/yyyy.
75 |
76 | Returns:
77 | str | None: A string with the date written out in Brazilian Portuguese,
78 | or None if the date is invalid.
79 |
80 | """
81 | if not DATE_REGEX.match(date):
82 | return None
83 |
84 | try:
85 | dt = datetime.strptime(date, "%d/%m/%Y")
86 | except ValueError:
87 | return None
88 |
89 | day, month, year = dt.day, dt.month, dt.year
90 |
91 | day_str = "Primeiro" if day == 1 else num2words(day, lang="pt")
92 | month_enum = MonthsEnum(month)
93 | year_str = num2words(year, lang="pt")
94 |
95 | return f"{day_str.capitalize()} de {month_enum.month_name} de {year_str}"
96 |
--------------------------------------------------------------------------------
/brutils/pis.py:
--------------------------------------------------------------------------------
1 | from random import randint
2 |
3 | WEIGHTS = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
4 |
5 | # FORMATTING
6 | ############
7 |
8 |
9 | def remove_symbols(pis: str) -> str:
10 | """
11 | Remove formatting symbols from a PIS.
12 |
13 | This function takes a PIS (Programa de Integração Social) string with
14 | formatting symbols and returns a cleaned version with no symbols.
15 |
16 | Args:
17 | pis (str): A PIS string that may contain formatting symbols.
18 |
19 | Returns:
20 | str: A cleaned PIS string with no formatting symbols.
21 |
22 | Example:
23 | >>> remove_symbols("123.456.789-09")
24 | '12345678909'
25 | >>> remove_symbols("98765432100")
26 | '98765432100'
27 | """
28 | return pis.replace(".", "").replace("-", "")
29 |
30 |
31 | def format_pis(pis: str) -> str:
32 | """
33 | Format a valid PIS (Programa de Integração Social) string with
34 | standard visual aid symbols.
35 |
36 | This function takes a valid numbers-only PIS string as input
37 | and adds standard formatting visual aid symbols for display.
38 |
39 | Args:
40 | pis (str): A valid numbers-only PIS string.
41 |
42 | Returns:
43 | str: A formatted PIS string with standard visual aid symbols
44 | or None if the input is invalid.
45 |
46 | Example:
47 | >>> format_pis("12345678909")
48 | '123.45678.90-9'
49 | >>> format_pis("98765432100")
50 | '987.65432.10-0'
51 | """
52 |
53 | if not is_valid(pis):
54 | return None
55 |
56 | return "{}.{}.{}-{}".format(pis[:3], pis[3:8], pis[8:10], pis[10:11])
57 |
58 |
59 | # OPERATIONS
60 | ############
61 |
62 |
63 | def is_valid(pis: str) -> bool:
64 | """
65 | Returns whether or not the verifying checksum digit of the
66 | given `PIS` match its base number.
67 |
68 | Args:
69 | pis (str): PIS number as a string of proper length.
70 |
71 | Returns:
72 | bool: True if PIS is valid, False otherwise.
73 |
74 | Example:
75 | >>> is_valid_pis("82178537464")
76 | True
77 | >>> is_valid_pis("55550207753")
78 | True
79 |
80 | """
81 |
82 | return (
83 | isinstance(pis, str)
84 | and len(pis) == 11
85 | and pis.isdigit()
86 | and pis[-1] == str(_checksum(pis[:-1]))
87 | )
88 |
89 |
90 | def generate() -> str:
91 | """
92 | Generate a random valid Brazilian PIS number.
93 |
94 | This function generates a random PIS number with the following characteristics:
95 | - It has 11 digits
96 | - It passes the weight calculation check
97 |
98 | Args:
99 | None
100 |
101 | Returns:
102 | str: A randomly generated valid PIS number as a string.
103 |
104 | Example:
105 | >>> generate()
106 | '12345678909'
107 | >>> generate()
108 | '98765432100'
109 | """
110 | base = str(randint(0, 9999999999)).zfill(10)
111 |
112 | return base + str(_checksum(base))
113 |
114 |
115 | def _checksum(base_pis: str) -> int:
116 | """
117 | Calculate the checksum digit of the given `base_pis` string.
118 |
119 | Args:
120 | base_pis (str): The first 10 digits of a PIS number as a string.
121 |
122 | Returns:
123 | int: The checksum digit.
124 | """
125 | pis_digits = list(map(int, base_pis))
126 | pis_sum = sum(digit * weight for digit, weight in zip(pis_digits, WEIGHTS))
127 | check_digit = 11 - (pis_sum % 11)
128 |
129 | return 0 if check_digit in [10, 11] else check_digit
130 |
--------------------------------------------------------------------------------
/brutils/ibge/uf.py:
--------------------------------------------------------------------------------
1 | import unicodedata
2 |
3 | from brutils.data.enums.uf import UF, UF_CODE
4 |
5 |
6 | def convert_code_to_uf(code: str) -> str | None:
7 | """
8 | Converts a given IBGE code (2-digit string) to its corresponding UF (state abbreviation).
9 |
10 | This function takes a 2-digit IBGE code and returns the corresponding UF code.
11 | It handles all Brazilian states and the Federal District.
12 |
13 | Args:
14 | code (str): The 2-digit IBGE code to be converted.
15 |
16 | Returns:
17 | str or None: The UF code corresponding to the IBGE code,
18 | or None if the IBGE code is invalid.
19 |
20 | Example:
21 | >>> convert_code_to_uf('12')
22 | 'AC'
23 | >>> convert_code_to_uf('33')
24 | 'RJ'
25 | >>> convert_code_to_uf('99')
26 | >>>
27 | """
28 | if code not in UF_CODE.values:
29 | return None
30 |
31 | return UF_CODE(code).name
32 |
33 |
34 | def convert_uf_to_name(uf: str) -> str | None:
35 | """
36 | Convert a Brazilian UF code (e.g., 'SP') to its full state name ('São Paulo').
37 |
38 | - The lookup is case-insensitive and ignores surrounding whitespace.
39 | - Returns ``None`` if the input is invalid or the UF code is not recognized.
40 |
41 | Args:
42 | uf (str): A two-letter UF code (e.g., 'RJ', 'sp').
43 |
44 | Returns:
45 | str | None: The full state name if found, or ``None`` if the code is invalid.
46 |
47 | Examples:
48 | >>> convert_uf_to_name('SP')
49 | 'São Paulo'
50 | >>> convert_uf_to_name('rj')
51 | 'Rio de Janeiro'
52 | """
53 | if not uf or not isinstance(uf, str) or len(uf.strip()) != 2:
54 | return None
55 |
56 | federal_unit = uf.strip().upper()
57 |
58 | if federal_unit not in UF.names:
59 | return None
60 |
61 | result = UF[federal_unit].value
62 |
63 | return result
64 |
65 |
66 | def _normalize_text(text: str) -> str:
67 | """
68 | Normalize text by removing accents and normalizing whitespace.
69 |
70 | Args:
71 | text (str): The text to normalize.
72 |
73 | Returns:
74 | str: The normalized text in uppercase.
75 | """
76 | nfd = unicodedata.normalize("NFD", text)
77 | without_accents = "".join(
78 | char for char in nfd if unicodedata.category(char) != "Mn"
79 | )
80 |
81 | normalized_spaces = " ".join(without_accents.split())
82 | return normalized_spaces.upper()
83 |
84 |
85 | def convert_name_to_uf(state_name: str) -> str | None:
86 | """
87 | Convert a Brazilian state name to its UF code.
88 |
89 | This function takes the full name of a Brazilian state and returns its
90 | corresponding two-letter UF code. The comparison is case-insensitive and
91 | ignores accents.
92 |
93 | Args:
94 | state_name (str): The full name of the state (e.g., 'São Paulo', 'sao paulo').
95 |
96 | Returns:
97 | str | None: The UF code if found, or None if the state name is invalid.
98 |
99 | Examples:
100 | >>> convert_name_to_uf('São Paulo')
101 | 'SP'
102 | >>> convert_name_to_uf('sao paulo')
103 | 'SP'
104 | >>> convert_name_to_uf('Rio de Janeiro')
105 | 'RJ'
106 | >>> convert_name_to_uf('rio de janeiro')
107 | 'RJ'
108 | >>> convert_name_to_uf('Estado Inválido')
109 | >>>
110 | """
111 | if not state_name or not isinstance(state_name, str):
112 | return None
113 |
114 | normalized_input = _normalize_text(state_name.strip())
115 |
116 | for uf in UF:
117 | if _normalize_text(uf.value) == normalized_input:
118 | return uf.name
119 |
120 | return None
121 |
--------------------------------------------------------------------------------
/brutils/currency.py:
--------------------------------------------------------------------------------
1 | from decimal import ROUND_DOWN, Decimal, InvalidOperation
2 |
3 | from num2words import num2words
4 |
5 |
6 | def format_currency(value: float | int | str | Decimal) -> str | None:
7 | """
8 | Format a numeric value as Brazilian currency (R$).
9 |
10 | Args:
11 | value: The numeric value to format. Accepts float, int, str, or Decimal.
12 |
13 | Returns:
14 | Formatted currency string (e.g., "R$ 1.234,56") or None if invalid.
15 |
16 | Example:
17 | >>> format_currency(1234.56)
18 | 'R$ 1.234,56'
19 | >>> format_currency(0)
20 | 'R$ 0,00'
21 | >>> format_currency(-9876.54)
22 | 'R$ -9.876,54'
23 | >>> format_currency("invalid")
24 | None
25 | """
26 | try:
27 | decimal_value = Decimal(value)
28 | formatted_value = (
29 | f"R$ {decimal_value:,.2f}".replace(",", "_")
30 | .replace(".", ",")
31 | .replace("_", ".")
32 | )
33 | return formatted_value
34 | except (InvalidOperation, TypeError, ValueError):
35 | return None
36 |
37 |
38 | def convert_real_to_text(amount: Decimal | float | int | str) -> str | None:
39 | """
40 | Convert a monetary value in Brazilian Reais to textual representation.
41 |
42 | Args:
43 | amount: Monetary value to convert. Accepts Decimal, float, int, or str.
44 |
45 | Returns:
46 | Textual representation in Brazilian Portuguese, or None if invalid.
47 |
48 | Note:
49 | - Values are rounded down to 2 decimal places
50 | - Maximum supported value is 1 quadrillion reais
51 | - Negative values are prefixed with "Menos"
52 |
53 | Example:
54 | >>> convert_real_to_text(1523.45)
55 | 'Mil, quinhentos e vinte e três reais e quarenta e cinco centavos'
56 | >>> convert_real_to_text(1.00)
57 | 'Um real'
58 | >>> convert_real_to_text(0.50)
59 | 'Cinquenta centavos'
60 | >>> convert_real_to_text(0.00)
61 | 'Zero reais'
62 | """
63 | try:
64 | amount = Decimal(str(amount)).quantize(
65 | Decimal("0.01"), rounding=ROUND_DOWN
66 | )
67 | except (InvalidOperation, TypeError, ValueError):
68 | return None
69 |
70 | if amount.is_nan() or amount.is_infinite():
71 | return None
72 |
73 | if abs(amount) > Decimal("1000000000000000.00"): # 1 quadrillion
74 | return None
75 |
76 | negative = amount < 0
77 | amount = abs(amount)
78 |
79 | reais = int(amount)
80 | centavos = int((amount - reais) * 100)
81 |
82 | parts = []
83 |
84 | if reais > 0:
85 | """
86 | Note:
87 | Although the `num2words` library provides a "to='currency'" feature, it has known
88 | issues with the representation of "zero reais" and "zero centavos". Therefore, this
89 | implementation uses only the traditional number-to-text conversion for better accuracy.
90 | """
91 | reais_text = num2words(reais, lang="pt_BR")
92 | currency_text = "real" if reais == 1 else "reais"
93 | conector = "de " if reais_text.endswith(("lhão", "lhões")) else ""
94 | parts.append(f"{reais_text} {conector}{currency_text}")
95 |
96 | if centavos > 0:
97 | centavos_text = f"{num2words(centavos, lang='pt_BR')} {'centavo' if centavos == 1 else 'centavos'}"
98 | if reais > 0:
99 | parts.append(f"e {centavos_text}")
100 | else:
101 | parts.append(centavos_text)
102 |
103 | if reais == 0 and centavos == 0:
104 | parts.append("Zero reais")
105 |
106 | result = " ".join(parts)
107 | if negative:
108 | result = f"Menos {result}"
109 |
110 | return result.capitalize()
111 |
--------------------------------------------------------------------------------
/tests/test_cpf.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 | from unittest.mock import patch
3 |
4 | from brutils.cpf import (
5 | _checksum,
6 | _hashdigit,
7 | display,
8 | format_cpf,
9 | generate,
10 | is_valid,
11 | remove_symbols,
12 | sieve,
13 | validate,
14 | )
15 |
16 |
17 | class TestCPF(TestCase):
18 | def test_sieve(self):
19 | self.assertEqual(sieve("00000000000"), "00000000000")
20 | self.assertEqual(sieve("123.456.789-10"), "12345678910")
21 | self.assertEqual(sieve("134..2435.-1892.-"), "13424351892")
22 | self.assertEqual(sieve("abc1230916*!*"), "abc1230916*!*")
23 | self.assertEqual(
24 | sieve("ab.c1.--.2-309.-1-.6-.*.-!*"), "abc1230916*!*"
25 | )
26 | self.assertEqual(sieve("...---..."), "")
27 |
28 | def test_display(self):
29 | self.assertEqual(display("00000000011"), "000.000.000-11")
30 | self.assertIsNone(display("00000000000"))
31 | self.assertIsNone(display("0000000000a"))
32 | self.assertIsNone(display("000000000000"))
33 |
34 | def test_validate(self):
35 | self.assertIs(validate("52513127765"), True)
36 | self.assertIs(validate("52599927765"), True)
37 | self.assertIs(validate("00000000000"), False)
38 |
39 | def test_is_valid(self):
40 | # When cpf is not string, returns False
41 | self.assertIs(is_valid(1), False)
42 |
43 | # When cpf's len is different of 11, returns False
44 | self.assertIs(is_valid("1"), False)
45 |
46 | # When cpf does not contain only digits, returns False
47 | self.assertIs(is_valid("1112223334-"), False)
48 |
49 | # When CPF has only the same digit, returns false
50 | self.assertIs(is_valid("11111111111"), False)
51 |
52 | # When rest_1 is lt 2 and the 10th digit is not 0, returns False
53 | self.assertIs(is_valid("11111111215"), False)
54 |
55 | # When rest_1 is gte 2 and the 10th digit is not (11 - rest), returns
56 | # False
57 | self.assertIs(is_valid("11144477705"), False)
58 |
59 | # When rest_2 is lt 2 and the 11th digit is not 0, returns False
60 | self.assertIs(is_valid("11111111204"), False)
61 |
62 | # When rest_2 is gte 2 and the 11th digit is not (11 - rest), returns
63 | # False
64 | self.assertIs(is_valid("11144477732"), False)
65 |
66 | # When cpf is valid
67 | self.assertIs(is_valid("11144477735"), True)
68 | self.assertIs(is_valid("11111111200"), True)
69 |
70 | def test_generate(self):
71 | for _ in range(10_000):
72 | self.assertIs(validate(generate()), True)
73 | self.assertIsNotNone(display(generate()))
74 |
75 | def test__hashdigit(self):
76 | self.assertEqual(_hashdigit("000000000", 10), 0)
77 | self.assertEqual(_hashdigit("0000000000", 11), 0)
78 | self.assertEqual(_hashdigit("52513127765", 10), 6)
79 | self.assertEqual(_hashdigit("52513127765", 11), 5)
80 |
81 | def test_checksum(self):
82 | self.assertEqual(_checksum("000000000"), "00")
83 | self.assertEqual(_checksum("525131277"), "65")
84 |
85 |
86 | @patch("brutils.cpf.sieve")
87 | class TestRemoveSymbols(TestCase):
88 | def test_remove_symbols(self, mock_sieve):
89 | # When call remove_symbols, it calls sieve
90 | remove_symbols("123.456.789-10")
91 | mock_sieve.assert_called()
92 |
93 |
94 | @patch("brutils.cpf.is_valid")
95 | class TestIsValidToFormat(TestCase):
96 | def test_when_cpf_is_valid_returns_true_to_format(self, mock_is_valid):
97 | mock_is_valid.return_value = True
98 |
99 | # When cpf is_valid, returns formatted cpf
100 | self.assertEqual(format_cpf("11144477735"), "111.444.777-35")
101 |
102 | # Checks if function is_valid_cpf is called
103 | mock_is_valid.assert_called_once_with("11144477735")
104 |
105 | def test_when_cpf_is_not_valid_returns_none(self, mock_is_valid):
106 | mock_is_valid.return_value = False
107 |
108 | # When cpf isn't valid, returns None
109 | self.assertIsNone(format_cpf("11144477735"))
110 |
111 |
112 | if __name__ == "__main__":
113 | main()
114 |
--------------------------------------------------------------------------------
/tests/test_cnpj.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 | from unittest.mock import patch
3 |
4 | from brutils.cnpj import (
5 | _checksum,
6 | _hashdigit,
7 | display,
8 | format_cnpj,
9 | generate,
10 | is_valid,
11 | remove_symbols,
12 | sieve,
13 | validate,
14 | )
15 |
16 |
17 | class TestCNPJ(TestCase):
18 | def test_sieve(self):
19 | self.assertEqual(sieve("00000000000"), "00000000000")
20 | self.assertEqual(sieve("12.345.678/0001-90"), "12345678000190")
21 | self.assertEqual(sieve("134..2435/.-1892.-"), "13424351892")
22 | self.assertEqual(sieve("abc1230916*!*"), "abc1230916*!*")
23 | self.assertEqual(
24 | sieve("ab.c1.--.2-3/09.-1-./6/-.*.-!*"), "abc1230916*!*"
25 | )
26 | self.assertEqual(sieve("/...---.../"), "")
27 |
28 | def test_display(self):
29 | self.assertEqual(display("00000000000109"), "00.000.000/0001-09")
30 | self.assertIsNone(display("00000000000000"))
31 | self.assertIsNone(display("0000000000000"))
32 | self.assertIsNone(display("0000000000000a"))
33 |
34 | def test_validate(self):
35 | self.assertIs(validate("34665388000161"), True)
36 | self.assertIs(validate("52599927000100"), False)
37 | self.assertIs(validate("00000000000"), False)
38 |
39 | def test_is_valid(self):
40 | # When CNPJ is not string, returns False
41 | self.assertIs(is_valid(1), False)
42 |
43 | # When CNPJ's len is different of 14, returns False
44 | self.assertIs(is_valid("1"), False)
45 |
46 | # When CNPJ does not contain only digits, returns False
47 | self.assertIs(is_valid("1112223334445-"), False)
48 |
49 | # When CNPJ has only the same digit, returns false
50 | self.assertIs(is_valid("11111111111111"), False)
51 |
52 | # When rest_1 is lt 2 and the 13th digit is not 0, returns False
53 | self.assertIs(is_valid("1111111111315"), False)
54 |
55 | # When rest_1 is gte 2 and the 13th digit is not (11 - rest), returns
56 | # False
57 | self.assertIs(is_valid("1111111111115"), False)
58 |
59 | # When rest_2 is lt 2 and the 14th digit is not 0, returns False
60 | self.assertIs(is_valid("11111111121205"), False)
61 |
62 | # When rest_2 is gte 2 and the 14th digit is not (11 - rest), returns
63 | # False
64 | self.assertIs(is_valid("11111111113105"), False)
65 |
66 | # When CNPJ is valid
67 | self.assertIs(is_valid("34665388000161"), True)
68 | self.assertIs(is_valid("01838723000127"), True)
69 |
70 | def test_generate(self):
71 | for _ in range(10_000):
72 | self.assertIs(validate(generate()), True)
73 | self.assertIsNotNone(display(generate()))
74 |
75 | def test__hashdigit(self):
76 | self.assertEqual(_hashdigit("00000000000000", 13), 0)
77 | self.assertEqual(_hashdigit("00000000000000", 14), 0)
78 | self.assertEqual(_hashdigit("52513127000292", 13), 9)
79 | self.assertEqual(_hashdigit("52513127000292", 14), 9)
80 |
81 | def test__checksum(self):
82 | self.assertEqual(_checksum("00000000000000"), "00")
83 | self.assertEqual(_checksum("52513127000299"), "99")
84 |
85 |
86 | @patch("brutils.cnpj.sieve")
87 | class TestRemoveSymbols(TestCase):
88 | def test_remove_symbols(self, mock_sieve):
89 | # When call remove_symbols, it calls sieve
90 | remove_symbols("12.345.678/0001-90")
91 | mock_sieve.assert_called()
92 |
93 |
94 | @patch("brutils.cnpj.is_valid")
95 | class TestIsValidToFormat(TestCase):
96 | def test_when_cnpj_is_valid_returns_true_to_format(self, mock_is_valid):
97 | mock_is_valid.return_value = True
98 |
99 | # When cnpj is_valid, returns formatted cnpj
100 | self.assertEqual(format_cnpj("01838723000127"), "01.838.723/0001-27")
101 |
102 | # Checks if function is_valid_cnpj is called
103 | mock_is_valid.assert_called_once_with("01838723000127")
104 |
105 | def test_when_cnpj_is_not_valid_returns_none(self, mock_is_valid):
106 | mock_is_valid.return_value = False
107 |
108 | # When cnpj isn't valid, returns None
109 | self.assertIsNone(format_cnpj("01838723000127"))
110 |
111 |
112 | if __name__ == "__main__":
113 | main()
114 |
--------------------------------------------------------------------------------
/tests/test_imports.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import inspect
3 | import pkgutil
4 | import unittest
5 |
6 |
7 | def get_public_functions(module):
8 | """Get all public functions and methods in the module."""
9 | return [
10 | name
11 | for name, obj in inspect.getmembers(module, inspect.isfunction)
12 | if not is_private_function(name) and inspect.getmodule(obj) == module
13 | ] + [
14 | name
15 | for name, obj in inspect.getmembers(module, inspect.ismethod)
16 | if obj.__module__ == module.__name__ and not name.startswith("_")
17 | ]
18 |
19 |
20 | def get_imported_methods(module):
21 | """Get all names in the module's namespace."""
22 | return [
23 | name
24 | for name in dir(module)
25 | if not is_private_function(name) and not is_standard_function(name)
26 | ]
27 |
28 |
29 | def is_private_function(name):
30 | """Check if a function is private."""
31 | return name.startswith("_")
32 |
33 |
34 | def is_standard_function(name):
35 | """Check if a function name is a standard or built-in function."""
36 | return name in dir(__builtins__) or (
37 | name.startswith("__") and name.endswith("__")
38 | )
39 |
40 |
41 | class TestImports(unittest.TestCase):
42 | @classmethod
43 | def setUpClass(cls):
44 | """Set up the package and its __init__.py module."""
45 | cls.package_name = "brutils"
46 | cls.package = importlib.import_module(cls.package_name)
47 | cls.init_module = importlib.import_module(
48 | f"{cls.package_name}.__init__"
49 | )
50 |
51 | cls.all_public_methods = []
52 | cls.imported_methods = []
53 |
54 | # Iterate over all submodules and collect their public methods
55 | for _, module_name, _ in pkgutil.walk_packages(
56 | cls.package.__path__, cls.package.__name__ + "."
57 | ):
58 | module = importlib.import_module(module_name)
59 | cls.all_public_methods.extend(get_public_functions(module))
60 |
61 | # Collect imported methods from __init__.py
62 | cls.imported_methods = get_imported_methods(cls.init_module)
63 |
64 | # Filter out standard or built-in functions
65 | cls.filtered_public_methods = [
66 | method
67 | for method in cls.all_public_methods
68 | if not is_standard_function(method)
69 | ]
70 | cls.filtered_imported_methods = [
71 | method
72 | for method in cls.imported_methods
73 | if not is_standard_function(method)
74 | ]
75 |
76 | # Remove specific old methods
77 | cls.filtered_public_methods = [
78 | method
79 | for method in cls.filtered_public_methods
80 | if method not in {"sieve", "display", "validate"}
81 | ]
82 |
83 | # Ensure all public methods are included in __all__
84 | cls.all_defined_names = dir(cls.init_module)
85 | cls.public_methods_in_all = getattr(cls.init_module, "__all__", [])
86 |
87 | cls.missing_imports = [
88 | method
89 | for method in cls.filtered_public_methods
90 | if method not in cls.filtered_imported_methods
91 | ]
92 |
93 | def test_public_methods_in_imports(self):
94 | """Test that all public methods are imported or aliased."""
95 | aliases_imports = [
96 | method
97 | for method in self.filtered_imported_methods
98 | if method not in self.filtered_public_methods
99 | ]
100 |
101 | diff = len(self.missing_imports) - len(aliases_imports)
102 |
103 | if diff != 0:
104 | self.fail(
105 | f"{diff} public method(s) missing from imports at __init__.py. You need to import the new brutils features methods inside the brutils/__init__.py file"
106 | )
107 |
108 | def test_public_methods_in_all(self):
109 | """Test that all public methods are included in __all__."""
110 | missing_in_all = set(self.filtered_imported_methods) - set(
111 | self.public_methods_in_all
112 | )
113 |
114 | if missing_in_all:
115 | self.fail(
116 | f"Public method(s) missing from __all__: {missing_in_all}. You need to add the new brutils features methods names to the list __all__ in the brutils/__init__.py file"
117 | )
118 |
119 |
120 | if __name__ == "__main__":
121 | unittest.main()
122 |
--------------------------------------------------------------------------------
/tests/phone/test_phone.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from brutils.phone import (
4 | _is_valid_landline,
5 | _is_valid_mobile,
6 | format_phone,
7 | generate,
8 | is_valid,
9 | remove_international_dialing_code,
10 | remove_symbols_phone,
11 | )
12 |
13 |
14 | class TestPhone(TestCase):
15 | def test_remove_symbols_phone(self):
16 | # When the string empty, it returns an empty string
17 | self.assertEqual(remove_symbols_phone(""), "")
18 |
19 | # When there are no symbols to remove, it returns the same string
20 | self.assertEqual(remove_symbols_phone("21994029275"), "21994029275")
21 |
22 | # When there are symbols to remove, it returns the string without
23 | # symbols
24 | self.assertEqual(remove_symbols_phone("(21)99402-9275"), "21994029275")
25 | self.assertEqual(remove_symbols_phone("(21)2569-6969"), "2125696969")
26 |
27 | # When there are extra symbols, it only removes the specified symbols
28 | self.assertEqual(
29 | remove_symbols_phone("(21) 99402-9275!"), "21994029275!"
30 | )
31 |
32 | # When the string contains non-numeric characters, it returns the
33 | # string without the specified symbols
34 | self.assertEqual(remove_symbols_phone("(21)ABC-DEF"), "21ABCDEF")
35 |
36 | # When the phone number contains a plus symbol and spaces, they are
37 | # removed
38 | self.assertEqual(
39 | remove_symbols_phone("+55 21 99402-9275"), "5521994029275"
40 | )
41 |
42 | # When the phone number contains multiple spaces, all are removed
43 | self.assertEqual(
44 | remove_symbols_phone("55 21 99402 9275"), "5521994029275"
45 | )
46 |
47 | # When the phone number contains a mixture of all specified symbols,
48 | # all are removed
49 | self.assertEqual(
50 | remove_symbols_phone("+55 (21) 99402-9275"), "5521994029275"
51 | )
52 |
53 | def test_format_phone_number(self):
54 | # When is a invalid number
55 | self.assertEqual(format_phone("333333"), None)
56 |
57 | # When is a mobile number
58 | self.assertEqual(format_phone("21994029275"), "(21)99402-9275")
59 | self.assertEqual(format_phone("21994029275"), "(21)99402-9275")
60 | self.assertEqual(format_phone("21994029275"), "(21)99402-9275")
61 | self.assertEqual(format_phone("11994029275"), "(11)99402-9275")
62 |
63 | # When is a landline number
64 | self.assertEqual(format_phone("1928814933"), "(19)2881-4933")
65 | self.assertEqual(format_phone("1938814933"), "(19)3881-4933")
66 | self.assertEqual(format_phone("1948814933"), "(19)4881-4933")
67 | self.assertEqual(format_phone("1958814933"), "(19)5881-4933")
68 | self.assertEqual(format_phone("3333333333"), "(33)3333-3333")
69 |
70 | def test_generate(self):
71 | for _ in range(25):
72 | with self.subTest():
73 | no_type_phone_generated = generate()
74 | self.assertIs(is_valid(no_type_phone_generated), True)
75 | mobile_phone_generated = generate("mobile")
76 | self.assertIs(_is_valid_mobile(mobile_phone_generated), True)
77 | landline_phone_generated = generate("landline")
78 | self.assertIs(
79 | _is_valid_landline(landline_phone_generated), True
80 | )
81 |
82 | def test_remove_international_dialing_code(self):
83 | # When the phone number does not have the international code,
84 | # return the same phone number
85 | self.assertEqual(
86 | remove_international_dialing_code("21994029275"), "21994029275"
87 | )
88 | self.assertEqual(
89 | remove_international_dialing_code("55994024478"), "55994024478"
90 | )
91 | self.assertEqual(
92 | remove_international_dialing_code("994024478"), "994024478"
93 | )
94 |
95 | # When the phone number has the international code,
96 | # return phone number without international code
97 | self.assertEqual(
98 | remove_international_dialing_code("5521994029275"), "21994029275"
99 | )
100 | self.assertEqual(
101 | remove_international_dialing_code("+5521994029275"), "+21994029275"
102 | )
103 | self.assertEqual(
104 | remove_international_dialing_code("+5555994029275"), "+55994029275"
105 | )
106 | self.assertEqual(
107 | remove_international_dialing_code("5511994029275"), "11994029275"
108 | )
109 |
110 |
111 | if __name__ == "__main__":
112 | main()
113 |
--------------------------------------------------------------------------------
/brutils/__init__.py:
--------------------------------------------------------------------------------
1 | # CEP Imports
2 | from brutils.cep import (
3 | format_cep,
4 | get_address_from_cep,
5 | get_cep_information_from_address,
6 | )
7 | from brutils.cep import generate as generate_cep
8 | from brutils.cep import is_valid as is_valid_cep
9 |
10 | # CNH Imports
11 | from brutils.cnh import is_valid_cnh as is_valid_cnh
12 |
13 | # CNPJ Imports
14 | from brutils.cnpj import format_cnpj
15 | from brutils.cnpj import generate as generate_cnpj
16 | from brutils.cnpj import is_valid as is_valid_cnpj
17 | from brutils.cnpj import remove_symbols as remove_symbols_cnpj
18 |
19 | # CPF Imports
20 | from brutils.cpf import format_cpf
21 | from brutils.cpf import generate as generate_cpf
22 | from brutils.cpf import is_valid as is_valid_cpf
23 | from brutils.cpf import remove_symbols as remove_symbols_cpf
24 |
25 | # Currency
26 | from brutils.currency import convert_real_to_text, format_currency
27 |
28 | # Date Utils Import
29 | from brutils.date_utils import convert_date_to_text, is_holiday
30 |
31 | # Email Import
32 | from brutils.email import is_valid as is_valid_email
33 |
34 | # IBGE Imports
35 | from brutils.ibge.municipality import (
36 | get_code_by_municipality_name,
37 | get_municipality_by_code,
38 | )
39 | from brutils.ibge.uf import (
40 | convert_code_to_uf,
41 | convert_name_to_uf,
42 | convert_uf_to_name,
43 | )
44 |
45 | # Legal Nature imports
46 | from brutils.legal_nature import get_description as get_natureza_legal_nature
47 | from brutils.legal_nature import is_valid as is_valid_legal_nature
48 | from brutils.legal_nature import list_all as list_all_legal_nature
49 |
50 | # Legal Process Imports
51 | from brutils.legal_process import format_legal_process
52 | from brutils.legal_process import generate as generate_legal_process
53 | from brutils.legal_process import is_valid as is_valid_legal_process
54 | from brutils.legal_process import remove_symbols as remove_symbols_legal_process
55 |
56 | # License Plate Imports
57 | from brutils.license_plate import (
58 | convert_to_mercosul as convert_license_plate_to_mercosul,
59 | )
60 | from brutils.license_plate import format_license_plate
61 | from brutils.license_plate import generate as generate_license_plate
62 | from brutils.license_plate import get_format as get_format_license_plate
63 | from brutils.license_plate import is_valid as is_valid_license_plate
64 | from brutils.license_plate import remove_symbols as remove_symbols_license_plate
65 |
66 | # Phone Imports
67 | from brutils.phone import (
68 | format_phone,
69 | remove_international_dialing_code,
70 | remove_symbols_phone,
71 | )
72 | from brutils.phone import generate as generate_phone
73 | from brutils.phone import is_valid as is_valid_phone
74 |
75 | # PIS Imports
76 | from brutils.pis import format_pis
77 | from brutils.pis import generate as generate_pis
78 | from brutils.pis import is_valid as is_valid_pis
79 | from brutils.pis import remove_symbols as remove_symbols_pis
80 |
81 | # RENAVAM Imports
82 | from brutils.renavam import is_valid_renavam
83 |
84 | # Voter ID Imports
85 | from brutils.voter_id import format_voter_id
86 | from brutils.voter_id import generate as generate_voter_id
87 | from brutils.voter_id import is_valid as is_valid_voter_id
88 |
89 | # Defining __all__ to expose the public methods
90 | __all__ = [
91 | # CEP
92 | "format_cep",
93 | "get_address_from_cep",
94 | "get_cep_information_from_address",
95 | "generate_cep",
96 | "is_valid_cep",
97 | # CNPJ
98 | "format_cnpj",
99 | "generate_cnpj",
100 | "is_valid_cnpj",
101 | "remove_symbols_cnpj",
102 | # CPF
103 | "format_cpf",
104 | "generate_cpf",
105 | "is_valid_cpf",
106 | "remove_symbols_cpf",
107 | # CNH
108 | "is_valid_cnh",
109 | # Email
110 | "is_valid_email",
111 | # Legal Process
112 | "format_legal_process",
113 | "generate_legal_process",
114 | "is_valid_legal_process",
115 | "remove_symbols_legal_process",
116 | # License Plate
117 | "convert_license_plate_to_mercosul",
118 | "format_license_plate",
119 | "generate_license_plate",
120 | "get_format_license_plate",
121 | "is_valid_license_plate",
122 | "remove_symbols_license_plate",
123 | # Phone
124 | "format_phone",
125 | "remove_international_dialing_code",
126 | "remove_symbols_phone",
127 | "generate_phone",
128 | "is_valid_phone",
129 | # PIS
130 | "format_pis",
131 | "generate_pis",
132 | "is_valid_pis",
133 | "remove_symbols_pis",
134 | # RENAVAM
135 | "is_valid_renavam",
136 | # Voter ID
137 | "format_voter_id",
138 | "generate_voter_id",
139 | "is_valid_voter_id",
140 | # IBGE
141 | "convert_code_to_uf",
142 | "convert_name_to_uf",
143 | "convert_uf_to_name",
144 | "get_code_by_municipality_name",
145 | "get_municipality_by_code",
146 | # Date Utils
147 | "is_holiday",
148 | "convert_date_to_text",
149 | # Currency
150 | "format_currency",
151 | "convert_real_to_text",
152 | # Legal Nature
153 | "is_valid_legal_nature",
154 | "get_natureza_legal_nature",
155 | "list_all_legal_nature",
156 | ]
157 |
--------------------------------------------------------------------------------
/tests/license_plate/test_license_plate.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 | from unittest.mock import patch
3 |
4 | from brutils.license_plate import (
5 | _is_valid_mercosul,
6 | _is_valid_old_format,
7 | convert_to_mercosul,
8 | format_license_plate,
9 | generate,
10 | get_format,
11 | remove_symbols,
12 | )
13 |
14 |
15 | class TestLicensePlate(TestCase):
16 | def test_remove_symbols(self):
17 | self.assertEqual(remove_symbols("ABC-123"), "ABC123")
18 | self.assertEqual(remove_symbols("abc123"), "abc123")
19 | self.assertEqual(remove_symbols("ABCD123"), "ABCD123")
20 | self.assertEqual(remove_symbols("A-B-C-1-2-3"), "ABC123")
21 | self.assertEqual(remove_symbols("@abc#-#123@"), "@abc##123@")
22 | self.assertEqual(remove_symbols("@---#"), "@#")
23 | self.assertEqual(remove_symbols("---"), "")
24 |
25 | def test_convert_license_plate_to_mercosul(self):
26 | # when license plate is not an instance of string, returns None
27 | self.assertIsNone(convert_to_mercosul(1234567))
28 |
29 | # when license plate has a length different from 7, returns None
30 | self.assertIsNone(convert_to_mercosul("ABC123"))
31 | self.assertIsNone(convert_to_mercosul("ABC12356"))
32 |
33 | # when then license plate's 5th character is not a number, return None
34 | self.assertIsNone(convert_to_mercosul("ABC1A34"))
35 | self.assertIsNone(convert_to_mercosul("ABC1-34"))
36 | self.assertIsNone(convert_to_mercosul("ABC1*34"))
37 | self.assertIsNone(convert_to_mercosul("ABC1_34"))
38 | self.assertIsNone(convert_to_mercosul("ABC1%34"))
39 | self.assertIsNone(convert_to_mercosul("ABC1 34"))
40 |
41 | # when then license plate's 5th character is 0, return with a letter A
42 | self.assertEqual(convert_to_mercosul("AAA1011"), "AAA1A11")
43 | # when then license plate's 5th character is 1, return with a letter B
44 | self.assertEqual(convert_to_mercosul("AAA1111"), "AAA1B11")
45 | # when then license plate's 5th character is 2, return with a letter C
46 | self.assertEqual(convert_to_mercosul("AAA1211"), "AAA1C11")
47 | # when then license plate's 5th character is 3, return with a letter D
48 | self.assertEqual(convert_to_mercosul("AAA1311"), "AAA1D11")
49 | # when then license plate's 5th character is 4, return with a letter E
50 | self.assertEqual(convert_to_mercosul("AAA1411"), "AAA1E11")
51 | # when then license plate's 5th character is 5, return with a letter F
52 | self.assertEqual(convert_to_mercosul("AAA1511"), "AAA1F11")
53 | # when then license plate's 5th character is 6, return with a letter G
54 | self.assertEqual(convert_to_mercosul("AAA1611"), "AAA1G11")
55 | # when then license plate's 5th character is 7, return with a letter H
56 | self.assertEqual(convert_to_mercosul("AAA1711"), "AAA1H11")
57 | # when then license plate's 5th character is 8, return with a letter I
58 | self.assertEqual(convert_to_mercosul("AAA1811"), "AAA1I11")
59 | # when then license plate's 5th character is 9, return with a letter J
60 | self.assertEqual(convert_to_mercosul("AAA1911"), "AAA1J11")
61 |
62 | # when then license is provided in lowercase, it's correctly converted
63 | # and then returned value is in uppercase
64 | self.assertEqual(convert_to_mercosul("abc1234"), "ABC1C34")
65 |
66 | def test_format_license_plate(self):
67 | self.assertEqual(format_license_plate("ABC1234"), "ABC-1234")
68 | self.assertEqual(format_license_plate("abc1234"), "ABC-1234")
69 | self.assertEqual(format_license_plate("ABC1D23"), "ABC1D23")
70 | self.assertEqual(format_license_plate("abc1d23"), "ABC1D23")
71 | self.assertIsNone(format_license_plate("ABCD123"))
72 |
73 | def test_get_format(self):
74 | # Old format
75 | self.assertEqual(get_format("ABC1234"), "LLLNNNN")
76 | self.assertEqual(get_format("abc1234"), "LLLNNNN")
77 |
78 | # Mercosul
79 | self.assertEqual(get_format("ABC4E67"), "LLLNLNN")
80 | self.assertEqual(get_format("XXX9X99"), "LLLNLNN")
81 |
82 | # Invalid
83 | self.assertIsNone(get_format(None))
84 | self.assertIsNone(get_format(""))
85 | self.assertIsNone(get_format("ABC-1D23"))
86 | self.assertIsNone(get_format("invalid plate"))
87 |
88 | def test_generate_license_plate(self):
89 | with patch("brutils.license_plate.choice", return_value="X"):
90 | with patch("brutils.license_plate.randint", return_value=9):
91 | self.assertEqual(generate(format="LLLNNNN"), "XXX9999")
92 | self.assertEqual(generate(format="LLLNLNN"), "XXX9X99")
93 |
94 | for _ in range(10_000):
95 | self.assertTrue(_is_valid_mercosul(generate(format="LLLNLNN")))
96 |
97 | for _ in range(10_000):
98 | self.assertTrue(_is_valid_old_format(generate(format="LLLNNNN")))
99 |
100 | # When no format is provided, returns a valid Mercosul license plate
101 | self.assertTrue(_is_valid_mercosul(generate()))
102 |
103 | # When invalid format is provided, returns None
104 | self.assertIsNone(generate("LNLNLNL"))
105 |
106 |
107 | if __name__ == "__main__":
108 | main()
109 |
--------------------------------------------------------------------------------
/tests/test_voter_id.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from brutils.voter_id import (
4 | _calculate_vd1,
5 | _calculate_vd2,
6 | _get_federative_union,
7 | _get_sequential_number,
8 | _get_verifying_digits,
9 | _is_length_valid,
10 | format_voter_id,
11 | generate,
12 | is_valid,
13 | )
14 |
15 |
16 | class TestIsValid(TestCase):
17 | def test_valid_voter_id(self):
18 | # test a valid voter id number
19 | voter_id = "217633460930"
20 | self.assertIs(is_valid(voter_id), True)
21 | self.assertIsInstance(is_valid(voter_id), bool)
22 |
23 | def test_invalid_voter_id(self):
24 | # test an invalid voter id number (dv1 & UF fail)
25 | voter_id = "123456789011"
26 | self.assertIs(is_valid(voter_id), False)
27 |
28 | def test_invalid_length(self):
29 | # Test an invalid length for voter id
30 | invalid_length_short = "12345678901"
31 | invalid_length_long = "1234567890123"
32 | self.assertIs(is_valid(invalid_length_short), False)
33 | self.assertIs(is_valid(invalid_length_long), False)
34 |
35 | def test_invalid_characters(self):
36 | # Test voter id with non-numeric characters
37 | invalid_characters = "ABCD56789012"
38 | invalid_characters_space = "217633 460 930"
39 | self.assertIs(is_valid(invalid_characters), False)
40 | self.assertIs(is_valid(invalid_characters_space), False)
41 |
42 | def test_valid_special_case(self):
43 | # Test a valid edge case (SP & MG with 13 digits)
44 | valid_special = "3244567800167"
45 | self.assertIs(is_valid(valid_special), True)
46 |
47 | def test_invalid_vd1(self):
48 | voter_id = "427503840223"
49 | self.assertIs(is_valid(voter_id), False)
50 |
51 | def test_invalid_vd2(self):
52 | voter_id = "427503840214"
53 | self.assertIs(is_valid(voter_id), False)
54 |
55 | def test_get_voter_id_parts(self):
56 | voter_id = "12345678AB12"
57 |
58 | sequential_number = _get_sequential_number(voter_id)
59 | federative_union = _get_federative_union(voter_id)
60 | verifying_digits = _get_verifying_digits(voter_id)
61 |
62 | self.assertEqual(sequential_number, "12345678")
63 | self.assertIsInstance(sequential_number, str)
64 |
65 | self.assertEqual(federative_union, "AB")
66 | self.assertIsInstance(federative_union, str)
67 |
68 | self.assertEqual(verifying_digits, "12")
69 | self.assertIsInstance(verifying_digits, str)
70 |
71 | def test_valid_length_verify(self):
72 | voter_id = "123456789012"
73 | self.assertIs(_is_length_valid(voter_id), True)
74 | self.assertIsInstance(_is_length_valid(voter_id), bool)
75 |
76 | def test_invalid_length_verify(self):
77 | voter_id = "12345678AB123" # Invalid length
78 | self.assertIs(_is_length_valid(voter_id), False)
79 |
80 | def test_calculate_vd1(self):
81 | self.assertIs(_calculate_vd1("07881476", "03"), 6)
82 |
83 | # test edge case: when federative union is SP and rest is 0, declare vd1 as 1
84 | self.assertIs(_calculate_vd1("73146499", "01"), 1)
85 | # test edge case: when federative union is MG and rest is 0, declare vd1 as 1
86 | self.assertIs(_calculate_vd1("42750359", "02"), 1)
87 | # test edge case: rest is 10, declare vd1 as 0
88 | self.assertIs(_calculate_vd1("73146415", "03"), 0)
89 | self.assertIsInstance(_calculate_vd1("73146415", "03"), int)
90 |
91 | def test_calculate_vd2(self):
92 | self.assertIs(_calculate_vd2("02", 7), 2)
93 | # edge case: if rest == 10, declare vd2 as zero
94 | self.assertIs(_calculate_vd2("03", 7), 0)
95 | # edge case: if UF is "01" (for SP) and rest == 0
96 | # declare dv2 as 1 instead
97 | self.assertIs(_calculate_vd2("01", 4), 1)
98 | # edge case: if UF is "02" (for MG) and rest == 0
99 | # declare dv2 as 1 instead
100 | self.assertIs(_calculate_vd2("02", 8), 1)
101 | self.assertIsInstance(_calculate_vd2("02", 8), int)
102 |
103 | def test_generate_voter_id(self):
104 | # test if is_valid a voter id from MG
105 | voter_id = generate(federative_union="MG")
106 | self.assertIs(is_valid(voter_id), True)
107 |
108 | # test if is_valid a voter id from AC
109 | voter_id = generate(federative_union="AC")
110 | self.assertIs(is_valid(voter_id), True)
111 |
112 | # test if is_valid a voter id from foreigner
113 | voter_id = generate()
114 | self.assertIs(is_valid(voter_id), True)
115 | self.assertIsInstance(voter_id, str)
116 |
117 | # test if UF is not valid
118 | voter_id = generate(federative_union="XX")
119 | self.assertIs(is_valid(voter_id), False)
120 | self.assertIsNone(voter_id)
121 |
122 | def test_format_voter_id(self):
123 | self.assertEqual(format_voter_id("277627122852"), "2776 2712 28 52")
124 | self.assertIsInstance(format_voter_id("277627122852"), str)
125 | self.assertIsNone(format_voter_id("00000000000"))
126 | self.assertIsNone(format_voter_id("0000000000a"))
127 | self.assertIsNone(format_voter_id("000000000000"))
128 | self.assertIsNone(format_voter_id("800911840197"))
129 |
130 |
131 | if __name__ == "__main__":
132 | main()
133 |
--------------------------------------------------------------------------------
/tests/test_currency.py:
--------------------------------------------------------------------------------
1 | from decimal import Decimal
2 | from unittest import TestCase
3 |
4 | from brutils.currency import convert_real_to_text, format_currency
5 |
6 |
7 | class TestFormatCurrency(TestCase):
8 | def test_when_value_is_a_decimal_value(self):
9 | assert format_currency(Decimal("123236.70")) == "R$ 123.236,70"
10 |
11 | def test_when_value_is_a_float_value(self):
12 | assert format_currency(123236.70) == "R$ 123.236,70"
13 |
14 | def test_when_value_is_negative(self):
15 | assert format_currency(-123236.70) == "R$ -123.236,70"
16 |
17 | def test_when_value_is_zero(self):
18 | assert format_currency(0) == "R$ 0,00"
19 |
20 | def test_value_decimal_replace_rounding(self):
21 | assert format_currency(-123236.7676) == "R$ -123.236,77"
22 |
23 | def test_when_value_is_not_a_valid_currency(self):
24 | assert format_currency("not a number") is None
25 | assert format_currency("09809,87") is None
26 | assert format_currency("897L") is None
27 |
28 |
29 | class TestConvertRealToText(TestCase):
30 | def test_convert_real_to_text(self):
31 | self.assertEqual(convert_real_to_text(0.00), "Zero reais")
32 | self.assertEqual(convert_real_to_text(0.01), "Um centavo")
33 | self.assertEqual(convert_real_to_text(0.50), "Cinquenta centavos")
34 | self.assertEqual(convert_real_to_text(1.00), "Um real")
35 | self.assertEqual(
36 | convert_real_to_text(-50.25),
37 | "Menos cinquenta reais e vinte e cinco centavos",
38 | )
39 | self.assertEqual(
40 | convert_real_to_text(1523.45),
41 | "Mil, quinhentos e vinte e três reais e quarenta e cinco centavos",
42 | )
43 | self.assertEqual(convert_real_to_text(1000000.00), "Um milhão de reais")
44 | self.assertEqual(
45 | convert_real_to_text(2000000.00), "Dois milhões de reais"
46 | )
47 | self.assertEqual(
48 | convert_real_to_text(1000000000.00), "Um bilhão de reais"
49 | )
50 | self.assertEqual(
51 | convert_real_to_text(2000000000.00), "Dois bilhões de reais"
52 | )
53 | self.assertEqual(
54 | convert_real_to_text(1000000000000.00), "Um trilhão de reais"
55 | )
56 | self.assertEqual(
57 | convert_real_to_text(2000000000000.00), "Dois trilhões de reais"
58 | )
59 | self.assertEqual(
60 | convert_real_to_text(1000000.45),
61 | "Um milhão de reais e quarenta e cinco centavos",
62 | )
63 | self.assertEqual(
64 | convert_real_to_text(2000000000.99),
65 | "Dois bilhões de reais e noventa e nove centavos",
66 | )
67 | self.assertEqual(
68 | convert_real_to_text(1234567890.50),
69 | "Um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa reais e cinquenta centavos",
70 | )
71 |
72 | # Almost zero values
73 | self.assertEqual(convert_real_to_text(0.001), "Zero reais")
74 | self.assertEqual(convert_real_to_text(0.009), "Zero reais")
75 |
76 | # Negative milions
77 | self.assertEqual(
78 | convert_real_to_text(-1000000.00), "Menos um milhão de reais"
79 | )
80 | self.assertEqual(
81 | convert_real_to_text(-2000000.50),
82 | "Menos dois milhões de reais e cinquenta centavos",
83 | )
84 |
85 | # billions with cents
86 | self.assertEqual(
87 | convert_real_to_text(1000000000.01),
88 | "Um bilhão de reais e um centavo",
89 | )
90 | self.assertEqual(
91 | convert_real_to_text(1000000000.99),
92 | "Um bilhão de reais e noventa e nove centavos",
93 | )
94 |
95 | self.assertEqual(
96 | convert_real_to_text(999999999999.99),
97 | "Novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos",
98 | )
99 |
100 | # trillions with cents
101 | self.assertEqual(
102 | convert_real_to_text(1000000000000.01),
103 | "Um trilhão de reais e um centavo",
104 | )
105 | self.assertEqual(
106 | convert_real_to_text(1000000000000.99),
107 | "Um trilhão de reais e noventa e nove centavos",
108 | )
109 | self.assertEqual(
110 | convert_real_to_text(9999999999999.99),
111 | "Nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos",
112 | )
113 |
114 | # 1 quadrillion
115 | self.assertEqual(
116 | convert_real_to_text(1000000000000000.00),
117 | "Um quatrilhão de reais",
118 | )
119 |
120 | # Edge cases should return None
121 | self.assertIsNone(
122 | convert_real_to_text("invalid_value")
123 | ) # invalid value
124 | self.assertIsNone(convert_real_to_text(None)) # None value
125 | self.assertIsNone(
126 | convert_real_to_text(-1000000000000001.00)
127 | ) # less than -1 quadrillion
128 | self.assertIsNone(
129 | convert_real_to_text(-1000000000000001.00)
130 | ) # more than 1 quadrillion
131 | self.assertIsNone(convert_real_to_text(float("inf"))) # Infinity
132 | self.assertIsNone(
133 | convert_real_to_text(float("nan"))
134 | ) # Not a number (NaN)
135 |
--------------------------------------------------------------------------------
/tests/ibge/test_uf.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from brutils.ibge.uf import (
4 | convert_code_to_uf,
5 | convert_name_to_uf,
6 | convert_uf_to_name,
7 | )
8 |
9 |
10 | class TestUF(TestCase):
11 | def test_convert_code_to_uf_valid(self):
12 | test_cases = [
13 | ("12", "AC"),
14 | ("27", "AL"),
15 | ("16", "AP"),
16 | ("13", "AM"),
17 | ("29", "BA"),
18 | ("23", "CE"),
19 | ("53", "DF"),
20 | ("32", "ES"),
21 | ("52", "GO"),
22 | ("21", "MA"),
23 | ("51", "MT"),
24 | ("50", "MS"),
25 | ("31", "MG"),
26 | ("15", "PA"),
27 | ("25", "PB"),
28 | ("41", "PR"),
29 | ("26", "PE"),
30 | ("22", "PI"),
31 | ("33", "RJ"),
32 | ("24", "RN"),
33 | ("43", "RS"),
34 | ("11", "RO"),
35 | ("14", "RR"),
36 | ("42", "SC"),
37 | ("35", "SP"),
38 | ("28", "SE"),
39 | ("17", "TO"),
40 | ]
41 | for code, expected_uf in test_cases:
42 | with self.subTest(code=code):
43 | self.assertEqual(convert_code_to_uf(code), expected_uf)
44 |
45 | def test_convert_code_to_uf_invalid(self):
46 | invalid_codes = ["99", "00", "", "AB", "1", "123"]
47 | for invalid_code in invalid_codes:
48 | with self.subTest(code=invalid_code):
49 | self.assertIsNone(convert_code_to_uf(invalid_code))
50 |
51 | def test_convert_uf_to_name_valid(self):
52 | test_cases = [
53 | ("SP", "São Paulo"),
54 | ("RJ", "Rio de Janeiro"),
55 | ("MG", "Minas Gerais"),
56 | ("DF", "Distrito Federal"),
57 | ("BA", "Bahia"),
58 | ("RS", "Rio Grande do Sul"),
59 | ]
60 | for uf, expected_name in test_cases:
61 | with self.subTest(uf=uf):
62 | self.assertEqual(convert_uf_to_name(uf), expected_name)
63 |
64 | def test_convert_uf_to_name_case_insensitive(self):
65 | test_cases = [
66 | ("sp", "São Paulo"),
67 | ("df", "Distrito Federal"),
68 | ("Rj", "Rio de Janeiro"),
69 | ]
70 | for uf, expected_name in test_cases:
71 | with self.subTest(uf=uf):
72 | self.assertEqual(convert_uf_to_name(uf), expected_name)
73 |
74 | def test_convert_uf_to_name_invalid(self):
75 | invalid_ufs = ["XX", "XXX", "", "A", "123", " "]
76 | for invalid_uf in invalid_ufs:
77 | with self.subTest(uf=invalid_uf):
78 | self.assertIsNone(convert_uf_to_name(invalid_uf))
79 |
80 | def test_convert_name_to_uf_all_states(self):
81 | test_cases = [
82 | ("Acre", "AC"),
83 | ("Alagoas", "AL"),
84 | ("Amapá", "AP"),
85 | ("Amazonas", "AM"),
86 | ("Bahia", "BA"),
87 | ("Ceará", "CE"),
88 | ("Distrito Federal", "DF"),
89 | ("Espírito Santo", "ES"),
90 | ("Goiás", "GO"),
91 | ("Maranhão", "MA"),
92 | ("Mato Grosso", "MT"),
93 | ("Mato Grosso do Sul", "MS"),
94 | ("Minas Gerais", "MG"),
95 | ("Pará", "PA"),
96 | ("Paraíba", "PB"),
97 | ("Paraná", "PR"),
98 | ("Pernambuco", "PE"),
99 | ("Piauí", "PI"),
100 | ("Rio de Janeiro", "RJ"),
101 | ("Rio Grande do Norte", "RN"),
102 | ("Rio Grande do Sul", "RS"),
103 | ("Rondônia", "RO"),
104 | ("Roraima", "RR"),
105 | ("Santa Catarina", "SC"),
106 | ("São Paulo", "SP"),
107 | ("Sergipe", "SE"),
108 | ("Tocantins", "TO"),
109 | ]
110 | for state_name, expected_uf in test_cases:
111 | with self.subTest(state_name=state_name):
112 | self.assertEqual(convert_name_to_uf(state_name), expected_uf)
113 |
114 | def test_convert_name_to_uf_normalization(self):
115 | test_cases = [
116 | ("sao paulo", "SP"),
117 | ("SAO PAULO", "SP"),
118 | ("SãO pAuLo", "SP"),
119 | (" Rio de Janeiro ", "RJ"),
120 | ("ceara", "CE"),
121 | ("PARANÁ", "PR"),
122 | ("parana", "PR"),
123 | ]
124 | for state_name, expected_uf in test_cases:
125 | with self.subTest(state_name=state_name):
126 | self.assertEqual(convert_name_to_uf(state_name), expected_uf)
127 |
128 | def test_convert_name_to_uf_wrong_spacing(self):
129 | test_cases = [
130 | ("Sao Paulo", "SP"),
131 | ("Rio de Janeiro", "RJ"),
132 | ("Mato Grosso", "MT"),
133 | ("Rio Grande do Sul", "RS"),
134 | ("Mato Grosso do Sul", "MS"),
135 | ("Espírito Santo", "ES"),
136 | ("Rio Grande do Norte", "RN"),
137 | ]
138 | for state_name, expected_uf in test_cases:
139 | with self.subTest(state_name=state_name):
140 | self.assertEqual(convert_name_to_uf(state_name), expected_uf)
141 |
142 | def test_convert_name_to_uf_invalid_input(self):
143 | invalid_inputs = [
144 | None,
145 | "",
146 | " ",
147 | "Estado Inválido",
148 | "São Pauloo",
149 | "Rio",
150 | "Brasil",
151 | "XYZ",
152 | 123,
153 | [],
154 | {},
155 | ]
156 | for invalid_input in invalid_inputs:
157 | with self.subTest(input=repr(invalid_input)):
158 | self.assertIsNone(convert_name_to_uf(invalid_input))
159 |
--------------------------------------------------------------------------------
/brutils/legal_process.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import re
4 | from datetime import datetime
5 | from random import randint
6 | from typing import Final
7 |
8 | ROOT_DIR: Final[str] = os.path.dirname(os.path.abspath(__file__))
9 | DATA_DIR: Final[str] = f"{ROOT_DIR}/data"
10 | VALID_IDS_FILE: Final[str] = f"{DATA_DIR}/legal_process_ids.json"
11 |
12 | # FORMATTING
13 | ############
14 |
15 |
16 | def remove_symbols(legal_process: str) -> str:
17 | """
18 | Removes specific symbols from a given legal process.
19 |
20 | This function takes a legal process as input and removes all occurrences
21 | of the '.' and '-' characters from it.
22 |
23 | Args:
24 | legal_process (str): A legal process containing symbols to be removed.
25 |
26 | Returns:
27 | str: The legal process string with the specified symbols removed.
28 |
29 | Example:
30 | >>> remove_symbols("123.45-678.901.234-56.7890")
31 | '12345678901234567890'
32 | >>> remove_symbols("9876543-21.0987.6.54.3210")
33 | '98765432109876543210'
34 | """
35 |
36 | return legal_process.replace(".", "").replace("-", "")
37 |
38 |
39 | def format_legal_process(legal_process_id: str) -> str | None:
40 | """
41 | Format a legal process ID into a standard format.
42 |
43 | Args:
44 | legal_process_id (str): A 20-digits string representing the legal
45 | process ID.
46 |
47 | Returns:
48 | str: The formatted legal process ID or None if the input is invalid.
49 |
50 | Example:
51 | >>> format_legal_process("12345678901234567890")
52 | '1234567-89.0123.4.56.7890'
53 | >>> format_legal_process("98765432109876543210")
54 | '9876543-21.0987.6.54.3210'
55 | >>> format_legal_process("123")
56 | None
57 | """
58 |
59 | if legal_process_id.isdigit() and len(legal_process_id) == 20:
60 | capture_fields = r"(\d{7})(\d{2})(\d{4})(\d)(\d{2})(\d{4})"
61 | include_chars = r"\1-\2.\3.\4.\5.\6"
62 |
63 | return re.sub(capture_fields, include_chars, legal_process_id)
64 |
65 | return None
66 |
67 |
68 | # OPERATIONS
69 | ############
70 |
71 |
72 | def is_valid(legal_process_id: str) -> bool:
73 | """
74 | Check if a legal process ID is valid.
75 |
76 | This function does not verify if the legal process ID is a real legal
77 | process ID; it only validates the format of the string.
78 |
79 | Args:
80 | legal_process_id (str): A digit-only string representing the legal
81 | process ID.
82 |
83 | Returns:
84 | bool: True if the legal process ID is valid, False otherwise.
85 |
86 | Example:
87 | >>> is_valid("68476506020233030000")
88 | True
89 | >>> is_valid("51808233620233030000")
90 | True
91 | >>> is_valid("123")
92 | False
93 | """
94 |
95 | clean_legal_process_id = remove_symbols(legal_process_id)
96 | DD = clean_legal_process_id[7:9]
97 | J = clean_legal_process_id[13:14]
98 | TR = clean_legal_process_id[14:16]
99 | OOOO = clean_legal_process_id[16:]
100 |
101 | with open(VALID_IDS_FILE) as file:
102 | legal_process_ids = json.load(file)
103 | process = legal_process_ids.get(f"orgao_{J}")
104 | if not process:
105 | return False
106 | valid_process = int(TR) in process.get("id_tribunal") and int(
107 | OOOO
108 | ) in process.get("id_foro")
109 |
110 | return (
111 | _checksum(int(clean_legal_process_id[0:7] + clean_legal_process_id[9:]))
112 | == DD
113 | ) and valid_process
114 |
115 |
116 | def generate(
117 | year: int = datetime.now().year, orgao: int = randint(1, 9)
118 | ) -> str | None:
119 | """
120 | Generate a random legal process ID number.
121 |
122 | Args:
123 | year (int): The year for the legal process ID (default is the current
124 | year).
125 | The year should not be in the past
126 | orgao (int): The organization code (1-9) for the legal process ID
127 | (default is random).
128 |
129 | Returns:
130 | str: A randomly generated legal process ID.
131 | None if one of the arguments is invalid.
132 |
133 | Example:
134 | >>> generate(2023, 5)
135 | '51659517020235080562'
136 | >>> generate()
137 | '88031888120233030000'
138 | >>> generate(2022, 10)
139 | None
140 | """
141 |
142 | if year < datetime.now().year or orgao not in range(1, 10):
143 | return None
144 |
145 | # Getting possible legal process ids from 'legal_process_ids.json' asset
146 | with open(VALID_IDS_FILE) as file:
147 | legal_process_ids = json.load(file)
148 | _ = legal_process_ids[f"orgao_{orgao}"]
149 | TR = str(
150 | _["id_tribunal"][randint(0, (len(_["id_tribunal"]) - 1))]
151 | ).zfill(2)
152 | OOOO = str(_["id_foro"][randint(0, (len(_["id_foro"])) - 1)]).zfill(4)
153 | NNNNNNN = str(randint(0, 9999999)).zfill(7)
154 | DD = _checksum(f"{NNNNNNN}{year}{orgao}{TR}{OOOO}")
155 |
156 | return f"{NNNNNNN}{DD}{year}{orgao}{TR}{OOOO}"
157 |
158 |
159 | def _checksum(basenum: int) -> str:
160 | """
161 | Checksum to compute the verification digit for a Legal Process ID number.
162 | `basenum` needs to be a digit without the verification id.
163 |
164 | Args:
165 | basenum (int): The base number for checksum calculation.
166 |
167 | Returns:
168 | str: The checksum value as a string.
169 |
170 | Example:
171 | >>> _checksum(1234567)
172 | '50'
173 | >>> _checksum(9876543)
174 | '88'
175 | """
176 |
177 | return str(97 - ((int(basenum) * 100) % 97)).zfill(2)
178 |
--------------------------------------------------------------------------------
/brutils/ibge/municipality.py:
--------------------------------------------------------------------------------
1 | import gzip
2 | import io
3 | import json
4 | import unicodedata
5 | from urllib.error import HTTPError
6 | from urllib.request import urlopen
7 |
8 | IBGE_MUNICIPALITY_BY_CODE_URL = (
9 | "https://servicodados.ibge.gov.br/api/v1/localidades/municipios/{code}"
10 | )
11 | IBGE_MUNICIPALITIES_BY_UF_URL = "https://servicodados.ibge.gov.br/api/v1/localidades/estados/{uf}/municipios"
12 |
13 |
14 | def get_municipality_by_code(code: str) -> tuple[str, str] | None:
15 | """
16 | Returns the municipality name and UF for a given IBGE code.
17 |
18 | This function takes a string representing an IBGE municipality code
19 | and returns a tuple with the municipality's name and its corresponding UF.
20 |
21 | Args:
22 | code (str): The IBGE code of the municipality.
23 |
24 | Returns:
25 | tuple: A tuple formatted as ("Município", "UF") or
26 | None if the code is not valid.
27 |
28 | Example:
29 | >>> get_municipality_by_code("3550308")
30 | ("São Paulo", "SP")
31 | >>> get_municipality_by_code("3304557")
32 | ("Rio de Janeiro", "RJ")
33 | >>> get_municipality_by_code("1234567")
34 | None
35 | """
36 | base_url = IBGE_MUNICIPALITY_BY_CODE_URL.format(code=code)
37 |
38 | decompressed_data = _fetch_ibge_data(base_url)
39 |
40 | if decompressed_data is None:
41 | return None
42 |
43 | try:
44 | json_data = json.loads(decompressed_data)
45 | return _get_values(json_data)
46 | except (json.JSONDecodeError, KeyError):
47 | return None
48 |
49 |
50 | def get_code_by_municipality_name(
51 | municipality_name: str, uf: str
52 | ) -> str | None:
53 | """
54 | Returns the IBGE code for a given municipality name and uf code.
55 |
56 | This function takes a string representing a municipality's name
57 | and uf's code and returns the corresponding IBGE code (string). The function
58 | will handle names by ignoring differences in case, accents, and
59 | treating the character ç as c and ignoring case differences for the uf code.
60 |
61 | Args:
62 | municipality_name (str): The name of the municipality.
63 | uf (str): The uf code of the state.
64 |
65 | Returns:
66 | str: The IBGE code of the municipality or
67 | None if the name is not valid or does not exist.
68 |
69 | Example:
70 | >>> get_code_by_municipality_name("São Paulo", "SP")
71 | "3550308"
72 | >>> get_code_by_municipality_name("Conceição do Coité", "Ba")
73 | "2908408"
74 | >>> get_code_by_municipality_name("Municipio Inexistente", "RS")
75 | None
76 | """
77 | uf = uf.upper()
78 |
79 | base_url = IBGE_MUNICIPALITIES_BY_UF_URL.format(uf=uf)
80 |
81 | decompressed_data = _fetch_ibge_data(base_url)
82 | if decompressed_data is None:
83 | return None
84 |
85 | try:
86 | json_data = json.loads(decompressed_data)
87 | normalized_municipality_name = _transform_text(municipality_name)
88 |
89 | for municipality in json_data:
90 | municipality_name_from_api = municipality.get("nome", "")
91 | normalized_name_from_api = _transform_text(
92 | municipality_name_from_api
93 | )
94 |
95 | if normalized_name_from_api == normalized_municipality_name:
96 | return str(municipality.get("id"))
97 |
98 | return None
99 |
100 | except (json.JSONDecodeError, KeyError):
101 | return None
102 |
103 |
104 | def _fetch_ibge_data(url: str) -> bytes | None:
105 | """
106 | Fetch data from IBGE API with gzip decompression support.
107 |
108 | Args:
109 | url (str): The URL to fetch data from.
110 |
111 | Returns:
112 | bytes | None: The decompressed data or None if failed.
113 | """
114 | try:
115 | with urlopen(url) as f:
116 | compressed_data = f.read()
117 | if f.info().get("Content-Encoding") == "gzip":
118 | try:
119 | with gzip.GzipFile(
120 | fileobj=io.BytesIO(compressed_data)
121 | ) as gzip_file:
122 | decompressed_data = gzip_file.read()
123 | except (OSError, Exception):
124 | return None
125 | else:
126 | decompressed_data = compressed_data
127 |
128 | if _is_empty(decompressed_data):
129 | return None
130 |
131 | return decompressed_data
132 |
133 | except HTTPError:
134 | return None
135 | except Exception:
136 | return None
137 |
138 |
139 | def _get_values(data: dict) -> tuple[str, str]:
140 | """Extract municipality name and UF from IBGE API response."""
141 | municipio = data["nome"]
142 | estado = data["microrregiao"]["mesorregiao"]["UF"]["sigla"]
143 | return (municipio, estado)
144 |
145 |
146 | def _is_empty(data: bytes) -> bool:
147 | """Check if the response data is empty."""
148 | return data == b"[]" or len(data) == 0
149 |
150 |
151 | def _transform_text(municipality_name: str) -> str:
152 | """
153 | Normalize municipality name and returns the normalized string.
154 |
155 | Args:
156 | municipality_name (str): The name of the municipality.
157 |
158 | Returns:
159 | str: The normalized string
160 |
161 | Example:
162 | >>> _transform_text("São Paulo")
163 | 'sao paulo'
164 | >>> _transform_text("Goiânia")
165 | 'goiania'
166 | >>> _transform_text("Conceição do Coité")
167 | 'conceicao do coite'
168 | """
169 | normalized_string = (
170 | unicodedata.normalize("NFKD", municipality_name)
171 | .encode("ascii", "ignore")
172 | .decode("ascii")
173 | )
174 | case_fold_string = normalized_string.casefold()
175 |
176 | return case_fold_string
177 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT_EN.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement via email to
64 | cmaiacd@gmail.com or mdeazevedomaia@gmail.com.
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/tests/phone/test_is_valid.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 | from unittest.mock import patch
3 |
4 | from brutils.phone import (
5 | _is_valid_landline,
6 | _is_valid_mobile,
7 | is_valid,
8 | )
9 |
10 |
11 | @patch("brutils.phone._is_valid_landline")
12 | @patch("brutils.phone._is_valid_mobile")
13 | class TestIsValidWithTypeMobile(TestCase):
14 | def test_when_mobile_is_valid_returns_true(
15 | self, mock__is_valid_mobile, mock__is_valid_landline
16 | ):
17 | mock__is_valid_mobile.return_value = True
18 |
19 | self.assertIs(is_valid("11994029275", "mobile"), True)
20 | mock__is_valid_mobile.assert_called_once_with("11994029275")
21 | mock__is_valid_landline.assert_not_called()
22 |
23 | def test_when_mobile_is_not_valid_returns_false(
24 | self, mock__is_valid_mobile, mock__is_valid_landline
25 | ):
26 | mock__is_valid_mobile.return_value = False
27 |
28 | self.assertIs(is_valid("119940", "mobile"), False)
29 | mock__is_valid_mobile.assert_called_once_with("119940")
30 | mock__is_valid_landline.assert_not_called()
31 |
32 |
33 | @patch("brutils.phone._is_valid_mobile")
34 | @patch("brutils.phone._is_valid_landline")
35 | class TestIsValidWithTypeLandline(TestCase):
36 | def test_when_landline_is_valid_returns_true(
37 | self, mock__is_valid_landline, mock__is_valid_mobile
38 | ):
39 | mock__is_valid_landline.return_value = True
40 |
41 | self.assertIs(is_valid("11994029275", "landline"), True)
42 | mock__is_valid_landline.assert_called_once_with("11994029275")
43 | mock__is_valid_mobile.assert_not_called()
44 |
45 | def test_when_landline_is_not_valid_returns_false(
46 | self, mock__is_valid_landline, mock__is_valid_mobile
47 | ):
48 | mock__is_valid_landline.return_value = False
49 |
50 | self.assertIs(is_valid("11994029275", "landline"), False)
51 | mock__is_valid_landline.assert_called_once_with("11994029275")
52 | mock__is_valid_mobile.assert_not_called()
53 |
54 |
55 | @patch("brutils.phone._is_valid_landline")
56 | @patch("brutils.phone._is_valid_mobile")
57 | class TestIsValidWithTypeNone(TestCase):
58 | def test_when_landline_is_valid(
59 | self, mock__is_valid_mobile, mock__is_valid_landline
60 | ):
61 | mock__is_valid_landline.return_value = True
62 |
63 | self.assertIs(is_valid("1958814933"), True)
64 | mock__is_valid_landline.assert_called_once_with("1958814933")
65 | mock__is_valid_mobile.assert_not_called()
66 |
67 | def test_when_landline_invalid_mobile_valid(
68 | self, mock__is_valid_mobile, mock__is_valid_landline
69 | ):
70 | mock__is_valid_landline.return_value = False
71 | mock__is_valid_mobile.return_value = True
72 |
73 | self.assertIs(is_valid("11994029275"), True)
74 | mock__is_valid_landline.assert_called_once_with("11994029275")
75 | mock__is_valid_mobile.assert_called_once_with("11994029275")
76 |
77 | def test_when_landline_and_mobile_are_invalid(
78 | self, mock__is_valid_mobile, mock__is_valid_landline
79 | ):
80 | mock__is_valid_landline.return_value = False
81 | mock__is_valid_mobile.return_value = False
82 |
83 | self.assertIs(is_valid("11994029275"), False)
84 | mock__is_valid_landline.assert_called_once_with("11994029275")
85 | mock__is_valid_mobile.assert_called_once_with("11994029275")
86 |
87 |
88 | class TestIsValidLandline(TestCase):
89 | def test__is_valid_landline(self):
90 | # When landline phone is not string, returns False
91 | self.assertIs(_is_valid_landline(1938814933), False)
92 |
93 | # When landline phone doesn't contain only digits, returns False
94 | self.assertIs(_is_valid_landline("(19)388149"), False)
95 |
96 | # When landline phone is an empty string, returns False
97 | self.assertIs(_is_valid_landline(""), False)
98 |
99 | # When landline phone's len is different of 10, returns False
100 | self.assertIs(_is_valid_landline("193881"), False)
101 |
102 | # When landline phone's first digit is 0, returns False
103 | self.assertIs(_is_valid_landline("0938814933"), False)
104 |
105 | # When landline phone's second digit is 0, returns False
106 | self.assertIs(_is_valid_landline("1038814933"), False)
107 |
108 | # When landline phone's third digit is different of 2,3,4 or 5,
109 | # returns False
110 | self.assertIs(_is_valid_landline("1998814933"), False)
111 |
112 | # When landline phone is valid
113 | self.assertIs(_is_valid_landline("1928814933"), True)
114 | self.assertIs(_is_valid_landline("1938814933"), True)
115 | self.assertIs(_is_valid_landline("1948814933"), True)
116 | self.assertIs(_is_valid_landline("1958814933"), True)
117 | self.assertIs(_is_valid_landline("3333333333"), True)
118 |
119 |
120 | class TestIsValidMobile(TestCase):
121 | def test__is_valid_mobile(self):
122 | # When mobile is not string, returns False
123 | self.assertIs(_is_valid_mobile(1), False)
124 |
125 | # When mobile doesn't contain only digits, returns False
126 | self.assertIs(_is_valid_mobile(119940 - 2927), False)
127 |
128 | # When mobile is an empty string, returns False
129 | self.assertIs(_is_valid_mobile(""), False)
130 |
131 | # When mobile's len is different of 11, returns False
132 | self.assertIs(_is_valid_mobile("119940"), False)
133 |
134 | # When mobile's first digit is 0, returns False
135 | self.assertIs(_is_valid_mobile("01994029275"), False)
136 |
137 | # When mobile's second digit is 0, returns False
138 | self.assertIs(_is_valid_mobile("90994029275"), False)
139 |
140 | # When mobile's third digit is different of 9, returns False
141 | self.assertIs(_is_valid_mobile("11594029275"), False)
142 |
143 | # When mobile is valid
144 | self.assertIs(_is_valid_mobile("99999999999"), True)
145 | self.assertIs(_is_valid_mobile("11994029275"), True)
146 |
147 |
148 | if __name__ == "__main__":
149 | main()
150 |
--------------------------------------------------------------------------------
/brutils/phone.py:
--------------------------------------------------------------------------------
1 | import re
2 | from random import choice, randint
3 |
4 |
5 | # FORMATTING
6 | ############
7 | def format_phone(phone: str) -> str | None:
8 | """
9 | Function responsible for formatting a telephone number
10 |
11 | Args:
12 | phone_number (str): The phone number to format.
13 |
14 | Returns:
15 | str: The formatted phone number, or None if the number is not valid.
16 |
17 |
18 | >>> format_phone("11994029275")
19 | '(11)99402-9275'
20 | >>> format_phone("1635014415")
21 | '(16)3501-4415'
22 | >>> format_phone("333333")
23 | """
24 | if not is_valid(phone):
25 | return None
26 |
27 | ddd = phone[:2]
28 | phone_number = phone[2:]
29 |
30 | return f"({ddd}){phone_number[:-4]}-{phone_number[-4:]}"
31 |
32 |
33 | # OPERATIONS
34 | ############
35 |
36 |
37 | def is_valid(phone_number: str, type: str = None) -> bool:
38 | """
39 | Returns if a Brazilian phone number is valid.
40 | It does not verify if the number actually exists.
41 |
42 | Args:
43 | phone_number (str): The phone number to validate.
44 | Only digits, without country code.
45 | It should include two digits DDD.
46 | type (str): "mobile" or "landline".
47 | If not specified, checks for one or another.
48 |
49 | Returns:
50 | bool: True if the phone number is valid. False otherwise.
51 | """
52 |
53 | if type == "landline":
54 | return _is_valid_landline(phone_number)
55 | if type == "mobile":
56 | return _is_valid_mobile(phone_number)
57 |
58 | return _is_valid_landline(phone_number) or _is_valid_mobile(phone_number)
59 |
60 |
61 | def remove_symbols_phone(phone_number: str) -> str:
62 | """
63 | Removes common symbols from a Brazilian phone number string.
64 |
65 | Args:
66 | phone_number (str): The phone number to remove symbols.
67 | Can include two digits DDD.
68 |
69 | Returns:
70 | str: A new string with the specified symbols removed.
71 | """
72 |
73 | cleaned_phone = (
74 | phone_number.replace("(", "")
75 | .replace(")", "")
76 | .replace("-", "")
77 | .replace("+", "")
78 | .replace(" ", "")
79 | )
80 | return cleaned_phone
81 |
82 |
83 | def generate(type: str = None) -> str:
84 | """
85 | Generate a valid and random phone number.
86 |
87 | Args:
88 | type (str): "landline" or "mobile".
89 | If not specified, checks for one or another.
90 |
91 | Returns:
92 | str: A randomly generated valid phone number.
93 |
94 | Example:
95 | >>> generate()
96 | "2234451215"
97 | >>> generate("mobile")
98 | "1899115895"
99 | >>> generate("landline")
100 | "5535317900"
101 | """
102 |
103 | if type == "mobile":
104 | return _generate_mobile_phone()
105 |
106 | if type == "landline":
107 | return _generate_landline_phone()
108 |
109 | generate_functions = [_generate_landline_phone, _generate_mobile_phone]
110 | return choice(generate_functions)()
111 |
112 |
113 | def remove_international_dialing_code(phone_number: str) -> str:
114 | """
115 | Function responsible for remove a international code phone
116 |
117 | Args:
118 | phone_number (str): The phone number with international code phone.
119 |
120 | Returns:
121 | str: The phone number without international code
122 | or the same phone number.
123 |
124 | Example:
125 | >>> remove_international_dialing_code("5511994029275")
126 | '11994029275'
127 | >>> remove_international_dialing_code("1635014415")
128 | '1635014415'
129 | >>> remove_international_dialing_code("+5511994029275")
130 | '+11994029275'
131 | """
132 |
133 | pattern = r"\+?55"
134 |
135 | if (
136 | re.search(pattern, phone_number)
137 | and len(phone_number.replace(" ", "")) > 11
138 | ):
139 | return phone_number.replace("55", "", 1)
140 | else:
141 | return phone_number
142 |
143 |
144 | def _is_valid_mobile(phone_number: str) -> bool:
145 | """
146 | Returns if a Brazilian mobile number is valid.
147 | It does not verify if the number actually exists.
148 |
149 | Args:
150 | phone_number (str): The mobile number to validate.
151 | Only digits, without country code.
152 | It should include two digits DDD.
153 |
154 | Returns:
155 | bool: True if the phone number is valid. False otherwise.
156 | """
157 |
158 | pattern = re.compile(r"^[1-9][1-9][9]\d{8}$")
159 | return (
160 | isinstance(phone_number, str)
161 | and re.match(pattern, phone_number) is not None
162 | )
163 |
164 |
165 | def _is_valid_landline(phone_number: str) -> bool:
166 | """
167 | Returns if a Brazilian landline number is valid.
168 | It does not verify if the number actually exists.
169 |
170 | Args:
171 | phone_number (str): The landline number to validate.
172 | Only digits, without country code.
173 | It should include two digits DDD.
174 |
175 | Returns:
176 | bool: True if the phone number is valid. False otherwise.
177 | """
178 |
179 | pattern = re.compile(r"^[1-9][1-9][2-5]\d{7}$")
180 | return (
181 | isinstance(phone_number, str)
182 | and re.match(pattern, phone_number) is not None
183 | )
184 |
185 |
186 | def _generate_ddd_number() -> str:
187 | """
188 | Generate a valid DDD number.
189 | """
190 | return f'{"".join([str(randint(1, 9)) for i in range(2)])}'
191 |
192 |
193 | def _generate_mobile_phone() -> str:
194 | """
195 | Generate a valid and random mobile phone number
196 | """
197 | ddd = _generate_ddd_number()
198 | client_number = [str(randint(0, 9)) for i in range(8)]
199 |
200 | phone_number = f'{ddd}9{"".join(client_number)}'
201 |
202 | return phone_number
203 |
204 |
205 | def _generate_landline_phone() -> str:
206 | """
207 | Generate a valid and random landline phone number.
208 | """
209 | ddd = _generate_ddd_number()
210 | return f"{ddd}{randint(2,5)}{str(randint(0,9999999)).zfill(7)}"
211 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Código de Conduta de Colaboração
3 |
4 | ## Nosso compromisso
5 |
6 | Como participantes, colaboradoras e líderes, nós nos comprometemos a fazer com que a participação
7 | em nossa comunidade seja uma experiência livre de assédio para todas as pessoas, independentemente
8 | de idade, tamanho do corpo, deficiência aparente ou não aparente, etnia, características sexuais,
9 | identidade ou expressão de gênero, nível de experiência, educação, situação sócio-econômica,
10 | nacionalidade, aparência pessoal, raça, casta, religião ou identidade e orientação sexuais.
11 |
12 | Comprometemo-nos a agir e interagir de maneiras que contribuam para uma comunidade aberta,
13 | acolhedora, diversificada, inclusiva e saudável.
14 |
15 | ## Nossos padrões
16 |
17 | Exemplos de comportamentos que contribuem para criar um ambiente positivo para a nossa comunidade
18 | incluem:
19 |
20 | * Demonstrar empatia e bondade com as outras pessoas
21 | * Respeitar opiniões, pontos de vista e experiências contrárias
22 | * Dar e receber feedbacks construtivos de maneira respeitosa
23 | * Assumir responsabilidade, pedir desculpas às pessoas afetadas por nossos erros e aprender com a
24 | experiência
25 | * Focar no que é melhor não só para nós individualmente, mas para a comunidade em geral
26 |
27 | Exemplos de comportamentos inaceitáveis incluem:
28 |
29 | * Uso de linguagem ou imagens sexualizadas, bem como o assédio sexual ou de qualquer natureza
30 | * Comentários insultuosos/depreciativos e ataques pessoais ou políticos (Trolling)
31 | * Assédio público ou privado
32 | * Publicar informações particulares de outras pessoas, como um endereço de e-mail ou endereço
33 | físico, sem a permissão explícita delas
34 | * Outras condutas que são normalmente consideradas inapropriadas em um ambiente profissional
35 |
36 | ## Aplicação das nossas responsabilidades
37 |
38 | A liderança da comunidade é responsável por esclarecer e aplicar nossos padrões de comportamento
39 | aceitáveis e tomará ações corretivas apropriadas e justas em resposta a qualquer comportamento que
40 | considerar impróprio, ameaçador, ofensivo ou problemático.
41 |
42 | A liderança da comunidade tem o direito e a responsabilidade de remover, editar ou rejeitar
43 | comentários, commits, códigos, edições na wiki, erros e outras contribuições que não estão
44 | alinhadas com este Código de Conduta e irá comunicar as razões por trás das decisões da moderação
45 | quando for apropriado.
46 |
47 | ## Escopo
48 |
49 | Este Código de Conduta se aplica dentro de todos os espaços da comunidade e também se aplica quando
50 | uma pessoa estiver representando oficialmente a comunidade em espaços públicos. Exemplos de
51 | representação da nossa comunidade incluem usar um endereço de e-mail oficial, postar em contas
52 | oficiais de mídias sociais ou atuar como uma pessoa indicada como representante em um evento
53 | online ou offline.
54 |
55 | ## Aplicação
56 |
57 | Ocorrências de comportamentos abusivos, de assédio ou que sejam inaceitáveis por qualquer outro
58 | motivo poderão ser reportadas para a liderança da comunidade, responsável pela aplicação, via
59 | contato cmaiacd@gmail.com ou mdeazevedomaia@gmail.com. Todas as reclamações serão revisadas e
60 | investigadas imediatamente e de maneira justa.
61 |
62 | A liderança da comunidade tem a obrigação de respeitar a privacidade e a segurança de quem reportar
63 | qualquer incidente.
64 |
65 | ## Diretrizes de aplicação
66 |
67 | A liderança da comunidade seguirá estas Diretrizes de Impacto na Comunidade para determinar as
68 | consequências de qualquer ação que considerar violadora deste Código de Conduta:
69 |
70 | ### 1. Ação Corretiva
71 |
72 | **Impacto na comunidade**: Uso de linguagem imprópria ou outro comportamento considerado
73 | anti-profissional ou repudiado pela comunidade.
74 |
75 | **Consequência**: Aviso escrito e privado da liderança da comunidade, esclarecendo a natureza da
76 | violação e com a explicação do motivo pelo qual o comportamento era impróprio. Um pedido de
77 | desculpas público poderá ser solicitado.
78 |
79 | ### 2. Advertência
80 |
81 | **Impacto na comunidade**: Violação por meio de um incidente único ou atitudes repetidas.
82 |
83 | **Consequência**: Advertência com consequências para comportamento repetido. Não poderá haver
84 | interações com as pessoas envolvidas, incluindo interações não solicitadas com as pessoas que
85 | estiverem aplicando o Código de Conduta, por um período determinado. Isto inclui evitar interações
86 | em espaços da comunidade, bem como canais externos como as mídias sociais. A violação destes termos
87 | pode levar a um banimento temporário ou permanente.
88 |
89 | ### 3. Banimento Temporário
90 |
91 | **Impacto na comunidade**: Violação grave dos padrões da comunidade, incluindo a persistência do
92 | comportamento impróprio.
93 |
94 | **Consequência**: Banimento temporário de qualquer tipo de interação ou comunicação pública com a
95 | comunidade por um determinado período. Estarão proibidas as interações públicas ou privadas com as
96 | pessoas envolvidas, incluindo interações não solicitadas com as pessoas que estiverem aplicando o
97 | Código de Conduta. A violação destes termos pode resultar em um banimento permanente.
98 |
99 | ### 4. Banimento Permanente
100 |
101 | **Impacto na comunidade**: Demonstrar um padrão na violação das normas da comunidade, incluindo a
102 | persistência do comportamento impróprio, assédio a uma pessoa ou agressão ou depreciação a classes
103 | de pessoas.
104 |
105 | **Consequência**: Banimento permanente de qualquer tipo de interação pública dentro da comunidade.
106 |
107 | ## Atribuição
108 |
109 | Este Código de Conduta é adaptado do [Contributor Covenant][homepage], versão 2.1, disponível em
110 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
111 |
112 | As Diretrizes de Impacto na Comunidade foram inspiradas pela
113 | [Aplicação do código de conduta Mozilla][Mozilla CoC].
114 |
115 | Para obter respostas a perguntas comuns sobre este código de conduta, veja a página de Perguntas
116 | Frequentes (FAQ) em [https://www.contributor-covenant.org/faq][FAQ]. Traduções estão disponíveis em
117 | [https://www.contributor-covenant.org/translations][translations].
118 |
119 | [homepage]: https://www.contributor-covenant.org
120 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
121 | [Mozilla CoC]: https://github.com/mozilla/diversity
122 | [FAQ]: https://www.contributor-covenant.org/faq
123 | [translations]: https://www.contributor-covenant.org/translations
124 |
--------------------------------------------------------------------------------
/brutils/legal_nature.py:
--------------------------------------------------------------------------------
1 | """
2 | brutils.legal_nature
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | Utilities for consulting and validating the official
6 | *Natureza Jurídica* (Legal Nature) codes defined by the
7 | Receita Federal do Brasil (RFB).
8 |
9 | .. note::
10 | The codes and descriptions in this module are sourced from the
11 | official **Tabela de Natureza Jurídica** (RFB), as provided in the
12 | document used by the Cadastro Nacional (e.g., FCN).
13 |
14 | This module offers simple lookups and validation helpers based on
15 | the official table. It does not infer the current legal/registration
16 | status of any entity.
17 |
18 | Source: https://www.gov.br/empresas-e-negocios/pt-br/drei/links-e-downloads/arquivos/TABELADENATUREZAJURDICA.pdf
19 | """
20 |
21 | from typing import Dict, Optional
22 |
23 | # FORMATTING
24 | ############
25 |
26 |
27 | # Helper to normalize inputs like "101-5" => "1015"
28 | def _normalize(code: str) -> Optional[str]:
29 | if not isinstance(code, str):
30 | return None
31 |
32 | digits = "".join(ch for ch in code.strip() if ch.isdigit())
33 |
34 | return digits if len(digits) == 4 else None
35 |
36 |
37 | LEGAL_NATURE: Dict[str, str] = {
38 | # 1. ADMINISTRAÇÃO PÚBLICA
39 | "1015": "Órgão Público do Poder Executivo Federal",
40 | "1023": "Órgão Público do Poder Executivo Estadual ou do Distrito Federal",
41 | "1031": "Órgão Público do Poder Executivo Municipal",
42 | "1040": "Órgão Público do Poder Legislativo Federal",
43 | "1058": "Órgão Público do Poder Legislativo Estadual ou do Distrito Federal",
44 | "1066": "Órgão Público do Poder Legislativo Municipal",
45 | "1074": "Órgão Público do Poder Judiciário Federal",
46 | "1082": "Órgão Público do Poder Judiciário Estadual",
47 | "1104": "Autarquia Federal",
48 | "1112": "Autarquia Estadual ou do Distrito Federal",
49 | "1120": "Autarquia Municipal",
50 | "1139": "Fundação Federal",
51 | "1147": "Fundação Estadual ou do Distrito Federal",
52 | "1155": "Fundação Municipal",
53 | "1163": "Órgão Público Autônomo da União",
54 | "1171": "Órgão Público Autônomo Estadual ou do Distrito Federal",
55 | "1180": "Órgão Público Autônomo Municipal",
56 | # 2. ENTIDADES EMPRESARIAIS
57 | "2011": "Empresa Pública",
58 | "2038": "Sociedade de Economia Mista",
59 | "2046": "Sociedade Anônima Aberta",
60 | "2054": "Sociedade Anônima Fechada",
61 | "2062": "Sociedade Empresária Limitada",
62 | "2076": "Sociedade Empresária em Nome Coletivo",
63 | "2089": "Sociedade Empresária em Comandita Simples",
64 | "2097": "Sociedade Empresária em Comandita por Ações",
65 | "2100": "Sociedade Mercantil de Capital e Indústria (extinta pelo NCC/2002)",
66 | "2127": "Sociedade Empresária em Conta de Participação",
67 | "2135": "Empresário (Individual)",
68 | "2143": "Cooperativa",
69 | "2151": "Consórcio de Sociedades",
70 | "2160": "Grupo de Sociedades",
71 | "2178": "Estabelecimento, no Brasil, de Sociedade Estrangeira",
72 | "2194": "Estabelecimento, no Brasil, de Empresa Binacional Argentino-Brasileira",
73 | "2208": "Entidade Binacional Itaipu",
74 | "2216": "Empresa Domiciliada no Exterior",
75 | "2224": "Clube/Fundo de Investimento",
76 | "2232": "Sociedade Simples Pura",
77 | "2240": "Sociedade Simples Limitada",
78 | "2259": "Sociedade em Nome Coletivo",
79 | "2267": "Sociedade em Comandita Simples",
80 | "2275": "Sociedade Simples em Conta de Participação",
81 | "2305": "Empresa Individual de Responsabilidade Limitada",
82 | # 3. ENTIDADES SEM FINS LUCRATIVOS
83 | "3034": "Serviço Notarial e Registral (Cartório)",
84 | "3042": "Organização Social",
85 | "3050": "Organização da Sociedade Civil de Interesse Público (Oscip)",
86 | "3069": "Outras Formas de Fundações Mantidas com Recursos Privados",
87 | "3077": "Serviço Social Autônomo",
88 | "3085": "Condomínio Edilícios",
89 | "3093": "Unidade Executora (Programa Dinheiro Direto na Escola)",
90 | "3107": "Comissão de Conciliação Prévia",
91 | "3115": "Entidade de Mediação e Arbitragem",
92 | "3123": "Partido Político",
93 | "3131": "Entidade Sindical",
94 | "3204": "Estabelecimento, no Brasil, de Fundação ou Associação Estrangeiras",
95 | "3212": "Fundação ou Associação Domiciliada no Exterior",
96 | "3999": "Outras Formas de Associação",
97 | # 4. PESSOAS FÍSICAS
98 | "4014": "Empresa Individual Imobiliária",
99 | "4022": "Segurado Especial",
100 | "4081": "Contribuinte individual",
101 | # 5. ORGANIZAÇÕES INTERNACIONAIS E OUTRAS INSTITUIÇÕES EXTRATERRITORIAIS
102 | "5002": "Organização Internacional e Outras Instituições Extraterritoriais",
103 | }
104 |
105 | # OPERATIONS
106 | ############
107 |
108 |
109 | def is_valid(code: str) -> bool:
110 | """
111 | Check if a string corresponds to a valid *Natureza Jurídica* code.
112 |
113 | Args:
114 | code (str): The code to be validated. Accepts either "NNNN" or "NNN-N".
115 |
116 | Returns:
117 | bool: True if the normalized code exists in the official table, False otherwise.
118 |
119 | Example:
120 | >>> is_valid("2062")
121 | True
122 | >>> is_valid("206-2")
123 | True
124 | >>> is_valid("9999")
125 | False
126 |
127 | .. note::
128 | Validation is based solely on the presence of the code in the
129 | official RFB table. It does not verify the current legal status
130 | or registration of the entity.
131 | """
132 | normalized = _normalize(code)
133 | return normalized in LEGAL_NATURE if normalized else False
134 |
135 |
136 | def get_description(code: str) -> Optional[str]:
137 | """
138 | Retrieve the description of a *Natureza Jurídica* code.
139 |
140 | Args:
141 | code (str): The code to look up. Accepts either "NNNN" or "NNN-N".
142 |
143 | Returns:
144 | str | None: The full description if the code is valid, otherwise None.
145 |
146 | Example:
147 | >>> get_description("2062")
148 | 'Sociedade Empresária Limitada'
149 | >>> get_description("101-5")
150 | 'Órgão Público do Poder Executivo Federal'
151 | >>> get_description("0000")
152 | None
153 | """
154 | normalized = _normalize(code)
155 | return LEGAL_NATURE.get(normalized) if normalized else None
156 |
157 |
158 | def list_all() -> Dict[str, str]:
159 | """
160 | Return a copy of the full *Natureza Jurídica* table.
161 |
162 | Returns:
163 | dict[str, str]: Mapping from "NNNN" codes to descriptions.
164 | """
165 | return dict(LEGAL_NATURE)
166 |
--------------------------------------------------------------------------------
/tests/license_plate/test_is_valid.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 | from unittest.mock import patch
3 |
4 | from brutils.license_plate import (
5 | _is_valid_mercosul,
6 | _is_valid_old_format,
7 | is_valid,
8 | )
9 |
10 |
11 | @patch("brutils.license_plate._is_valid_mercosul")
12 | @patch("brutils.license_plate._is_valid_old_format")
13 | class TestIsValidWithTypeOldFormat(TestCase):
14 | def test_when_old_format_is_valid_returns_true(
15 | self, mock__is_valid_old_format, mock__is_valid_mercosul
16 | ):
17 | mock__is_valid_old_format.return_value = True
18 |
19 | self.assertIs(is_valid("ABC1234", "old_format"), True)
20 | mock__is_valid_old_format.assert_called_once_with("ABC1234")
21 | mock__is_valid_mercosul.assert_not_called()
22 |
23 | def test_when_old_format_is_not_valid_returns_false(
24 | self, mock__is_valid_old_format, mock__is_valid_mercosul
25 | ):
26 | mock__is_valid_old_format.return_value = False
27 |
28 | self.assertIs(is_valid("123456", "old_format"), False)
29 | mock__is_valid_old_format.assert_called_once_with("123456")
30 | mock__is_valid_mercosul.assert_not_called()
31 |
32 |
33 | @patch("brutils.license_plate._is_valid_mercosul")
34 | @patch("brutils.license_plate._is_valid_old_format")
35 | class TestIsValidWithTypeMercosul(TestCase):
36 | def test_when_mercosul_is_valid_returns_true(
37 | self, mock__is_valid_old_format, mock__is_valid_mercosul
38 | ):
39 | mock__is_valid_mercosul.return_value = True
40 |
41 | self.assertIs(is_valid("ABC4E67", "mercosul"), True)
42 | mock__is_valid_mercosul.assert_called_once_with("ABC4E67")
43 | mock__is_valid_old_format.assert_not_called()
44 |
45 | def test_when_mercosul_is_not_valid_returns_false(
46 | self, mock__is_valid_old_format, mock__is_valid_mercosul
47 | ):
48 | mock__is_valid_mercosul.return_value = False
49 |
50 | self.assertIs(is_valid("11994029275", "mercosul"), False)
51 | mock__is_valid_mercosul.assert_called_once_with("11994029275")
52 | mock__is_valid_old_format.assert_not_called()
53 |
54 |
55 | @patch("brutils.license_plate._is_valid_mercosul")
56 | @patch("brutils.license_plate._is_valid_old_format")
57 | class TestIsValidWithTypeNone(TestCase):
58 | def test_when_mercosul_valid_old_format_invalid(
59 | self, mock__is_valid_old_format, mock__is_valid_mercosul
60 | ):
61 | mock__is_valid_mercosul.return_value = True
62 | mock__is_valid_old_format.return_value = False
63 |
64 | self.assertIs(is_valid("ABC4E67"), True)
65 | mock__is_valid_old_format.assert_called_once_with("ABC4E67")
66 | mock__is_valid_mercosul.assert_called_once_with("ABC4E67")
67 |
68 | def test_when_mercosul_and_old_format_are_valids(
69 | self, mock__is_valid_old_format, mock__is_valid_mercosul
70 | ):
71 | mock__is_valid_mercosul.return_value = True
72 | mock__is_valid_old_format.return_value = True
73 |
74 | self.assertIs(is_valid("ABC1234"), True)
75 | mock__is_valid_mercosul.assert_not_called()
76 | mock__is_valid_old_format.assert_called_once_with("ABC1234")
77 |
78 | def test_when_mercosul_invalid_old_format_valid(
79 | self, mock__is_valid_old_format, mock__is_valid_mercosul
80 | ):
81 | mock__is_valid_mercosul.return_value = False
82 | mock__is_valid_old_format.return_value = True
83 |
84 | self.assertIs(is_valid("ABC1234"), True)
85 | mock__is_valid_old_format.assert_called_once_with("ABC1234")
86 | mock__is_valid_mercosul.assert_not_called()
87 |
88 | def test_when_mercosul_and_old_format_are_invalid(
89 | self, mock__is_valid_old_format, mock__is_valid_mercosul
90 | ):
91 | mock__is_valid_old_format.return_value = False
92 | mock__is_valid_mercosul.return_value = False
93 |
94 | self.assertIs(is_valid("ABC1234"), False)
95 | mock__is_valid_old_format.assert_called_once_with("ABC1234")
96 | mock__is_valid_mercosul.assert_called_once_with("ABC1234")
97 |
98 |
99 | class TestIsValidOldFormat(TestCase):
100 | def test__is_valid_old_format(self):
101 | # When license plate is valid, returns True
102 | self.assertIs(_is_valid_old_format("ABC1234"), True)
103 | self.assertIs(_is_valid_old_format("abc1234"), True)
104 |
105 | # When license plate is valid with whitespaces, returns True
106 | self.assertIs(_is_valid_old_format(" ABC1234 "), True)
107 |
108 | # When license plate is not string, returns False
109 | self.assertIs(_is_valid_old_format(123456), False)
110 |
111 | # When license plate is invalid with special characters,
112 | # returns False
113 | self.assertIs(_is_valid_old_format("ABC-1234"), False)
114 |
115 | # When license plate is invalid with numbers and letters out of
116 | # order, returns False
117 | self.assertIs(_is_valid_old_format("A1CA23W"), False)
118 |
119 | # When license plate is invalid with new format, returns False
120 | self.assertIs(_is_valid_old_format("ABC1D23"), False)
121 | self.assertIs(_is_valid_old_format("abcd123"), False)
122 |
123 |
124 | class TestIsValidMercosul(TestCase):
125 | def test__is_valid_mercosul(self):
126 | # When license plate is not string, returns False
127 | self.assertIs(_is_valid_mercosul(1234567), False)
128 |
129 | # When license plate doesn't match the pattern LLLNLNN,
130 | # returns False
131 | self.assertIs(_is_valid_mercosul("ABCDEFG"), False)
132 | self.assertIs(_is_valid_mercosul("1234567"), False)
133 | self.assertIs(_is_valid_mercosul("ABC4567"), False)
134 | self.assertIs(_is_valid_mercosul("ABCD567"), False)
135 | self.assertIs(_is_valid_mercosul("ABC45F7"), False)
136 | self.assertIs(_is_valid_mercosul("ABC456G"), False)
137 | self.assertIs(_is_valid_mercosul("ABC123"), False)
138 |
139 | # When license plate is an empty string, returns False
140 | self.assertIs(_is_valid_mercosul(""), False)
141 |
142 | # When license plate's length is different of 7, returns False
143 | self.assertIs(_is_valid_mercosul("ABC4E678"), False)
144 |
145 | # When license plate has separator, returns false
146 | self.assertIs(_is_valid_mercosul("ABC-1D23"), False)
147 |
148 | # When license plate is valid
149 | self.assertIs(_is_valid_mercosul("ABC4E67"), True)
150 | self.assertIs(_is_valid_mercosul("AAA1A11"), True)
151 | self.assertIs(_is_valid_mercosul("XXX9X99"), True)
152 |
153 | # Check if function is case insensitive
154 | self.assertIs(_is_valid_mercosul("abc4e67"), True)
155 |
156 |
157 | if __name__ == "__main__":
158 | main()
159 |
--------------------------------------------------------------------------------
/brutils/cpf.py:
--------------------------------------------------------------------------------
1 | from random import randint
2 |
3 | # FORMATTING
4 | ############
5 |
6 |
7 | def sieve(dirty: str) -> str:
8 | """
9 | Removes specific symbols from a CPF (Brazilian Individual Taxpayer Number)
10 | string.
11 |
12 | This function takes a CPF string as input and removes all occurrences of
13 | the '.', '-' characters from it.
14 |
15 | Args:
16 | cpf (str): The CPF string containing symbols to be removed.
17 |
18 | Returns:
19 | str: A new string with the specified symbols removed.
20 |
21 | Example:
22 | >>> sieve("123.456.789-01")
23 | '12345678901'
24 | >>> sieve("987-654-321.01")
25 | '98765432101'
26 |
27 | .. note::
28 | This method should not be used in new code and is only provided for
29 | backward compatibility.
30 | """
31 |
32 | return "".join(filter(lambda char: char not in ".-", dirty))
33 |
34 |
35 | def remove_symbols(dirty: str) -> str:
36 | """
37 | Alias for the `sieve` function. Better naming.
38 |
39 | Args:
40 | cpf (str): The CPF string containing symbols to be removed.
41 |
42 | Returns:
43 | str: A new string with the specified symbols removed.
44 | """
45 |
46 | return sieve(dirty)
47 |
48 |
49 | def display(cpf: str) -> str:
50 | """
51 | Format a CPF for display with visual aid symbols.
52 |
53 | This function takes a numbers-only CPF string as input and adds standard
54 | formatting visual aid symbols for display.
55 |
56 | Args:
57 | cpf (str): A numbers-only CPF string.
58 |
59 | Returns:
60 | str: A formatted CPF string with standard visual aid symbols
61 | or None if the input is invalid.
62 |
63 | Example:
64 | >>> display("12345678901")
65 | "123.456.789-01"
66 | >>> display("98765432101")
67 | "987.654.321-01"
68 |
69 | .. note::
70 | This method should not be used in new code and is only provided for
71 | backward compatibility.
72 | """
73 |
74 | if not cpf.isdigit() or len(cpf) != 11 or len(set(cpf)) == 1:
75 | return None
76 |
77 | return "{}.{}.{}-{}".format(cpf[:3], cpf[3:6], cpf[6:9], cpf[9:])
78 |
79 |
80 | def format_cpf(cpf: str) -> str:
81 | """
82 | Format a CPF for display with visual aid symbols.
83 |
84 | This function takes a numbers-only CPF string as input and adds standard
85 | formatting visual aid symbols for display.
86 |
87 | Args:
88 | cpf (str): A numbers-only CPF string.
89 |
90 | Returns:
91 | str: A formatted CPF string with standard visual aid symbols or None
92 | if the input is invalid.
93 |
94 | Example:
95 | >>> format_cpf("82178537464")
96 | '821.785.374-64'
97 | >>> format_cpf("55550207753")
98 | '555.502.077-53'
99 | """
100 |
101 | if not is_valid(cpf):
102 | return None
103 |
104 | return "{}.{}.{}-{}".format(cpf[:3], cpf[3:6], cpf[6:9], cpf[9:11])
105 |
106 |
107 | # OPERATIONS
108 | ############
109 |
110 |
111 | def validate(cpf: str) -> bool:
112 | """
113 | Validate the checksum digits of a CPF.
114 |
115 | This function checks whether the verifying checksum digits of the given CPF
116 | match its base number. The input should be a digit string of the proper
117 | length.
118 |
119 | Args:
120 | cpf (str): A numbers-only CPF string.
121 |
122 | Returns:
123 | bool: True if the checksum digits are valid, False otherwise.
124 |
125 | Example:
126 | >>> validate("82178537464")
127 | True
128 | >>> validate("55550207753")
129 | True
130 |
131 | .. note::
132 | This method should not be used in new code and is only provided for
133 | backward compatibility.
134 | """
135 |
136 | if not cpf.isdigit() or len(cpf) != 11 or len(set(cpf)) == 1:
137 | return False
138 |
139 | return all(_hashdigit(cpf, i + 10) == int(v) for i, v in enumerate(cpf[9:]))
140 |
141 |
142 | def is_valid(cpf: str) -> bool:
143 | """
144 | Returns whether or not the verifying checksum digits of the given `˜CPF`
145 | match its base number.
146 |
147 | This function does not verify the existence of the CPF; it only
148 | validates the format of the string.
149 |
150 | Args:
151 | cpf (str): The CPF to be validated, a 11-digit string
152 |
153 | Returns:
154 | bool: True if the checksum digits match the base number,
155 | False otherwise.
156 |
157 | Example:
158 | >>> is_valid("82178537464")
159 | True
160 | >>> is_valid("55550207753")
161 | True
162 | """
163 |
164 | return isinstance(cpf, str) and validate(cpf)
165 |
166 |
167 | def generate() -> str:
168 | """
169 | Generate a random valid CPF digit string.
170 |
171 | This function generates a random valid CPF string.
172 |
173 | Returns:
174 | str: A random valid CPF string.
175 |
176 | Example:
177 | >>> generate()
178 | "10895948109"
179 | >>> generate()
180 | "52837606502"
181 | """
182 |
183 | base = str(randint(1, 999999998)).zfill(9)
184 |
185 | return base + _checksum(base)
186 |
187 |
188 | def _hashdigit(cpf: str, position: int) -> int:
189 | """
190 | Compute the given position checksum digit for a CPF.
191 |
192 | This function computes the specified position checksum digit for the CPF
193 | input.
194 | The input needs to contain all elements previous to the position, or the
195 | computation will yield the wrong result.
196 |
197 | Args:
198 | cpf (str): A CPF string.
199 | position (int): The position to calculate the checksum digit for.
200 |
201 | Returns:
202 | int: The calculated checksum digit.
203 |
204 | Example:
205 | >>> _hashdigit("52599927765", 11)
206 | 5
207 | >>> _hashdigit("52599927765", 10)
208 | 6
209 | """
210 |
211 | val = (
212 | sum(
213 | int(digit) * weight
214 | for digit, weight in zip(cpf, range(position, 1, -1))
215 | )
216 | % 11
217 | )
218 |
219 | return 0 if val < 2 else 11 - val
220 |
221 |
222 | def _checksum(basenum: str) -> str:
223 | """
224 | Compute the checksum digits for a given CPF base number.
225 |
226 | This function calculates the checksum digits for a given CPF base number.
227 | The base number should be a digit string of adequate length.
228 |
229 | Args:
230 | basenum (str): A digit string of adequate length.
231 |
232 | Returns:
233 | str: The calculated checksum digits.
234 |
235 | Example:
236 | >>> _checksum("335451269")
237 | '51'
238 | >>> _checksum("382916331")
239 | '26'
240 | """
241 |
242 | verifying_digits = str(_hashdigit(basenum, 10))
243 | verifying_digits += str(_hashdigit(basenum + verifying_digits, 11))
244 |
245 | return verifying_digits
246 |
--------------------------------------------------------------------------------
/brutils/license_plate.py:
--------------------------------------------------------------------------------
1 | import re
2 | from random import choice, randint
3 | from string import ascii_uppercase
4 | from typing import Literal
5 |
6 | # FORMATTING
7 | ############
8 |
9 |
10 | def convert_to_mercosul(license_plate: str) -> str | None:
11 | """
12 | Converts an old pattern license plate (LLLNNNN) to a Mercosul format
13 | (LLLNLNN).
14 |
15 | Args:
16 | license_plate (str): A string of proper length representing the
17 | old pattern license plate.
18 |
19 | Returns:
20 | str | None: The converted Mercosul license plate (LLLNLNN) or
21 | 'None' if the input is invalid.
22 |
23 | Example:
24 | >>> convert_to_mercosul("ABC4567")
25 | 'ABC4F67'
26 | >>> convert_to_mercosul("ABC4*67")
27 | None
28 | """
29 | if not _is_valid_old_format(license_plate):
30 | return None
31 |
32 | digits = [letter for letter in license_plate.upper()]
33 | digits[4] = chr(ord("A") + int(digits[4]))
34 | return "".join(digits)
35 |
36 |
37 | def format_license_plate(license_plate: str) -> str | None:
38 | """
39 | Formats a license plate into the correct pattern.
40 | This function receives a license plate in any pattern (LLLNNNN or LLLNLNN)
41 | and returns a formatted version.
42 |
43 | Args:
44 | license_plate (str): A license plate string.
45 |
46 | Returns:
47 | str | None: The formatted license plate string or 'None' if the
48 | input is invalid.
49 |
50 | Example:
51 | >>> format("ABC1234") # old format (contains a dash)
52 | 'ABC-1234'
53 | >>> format("abc1e34") # mercosul format
54 | 'ABC1E34'
55 | >>> format("ABC123")
56 | None
57 | """
58 |
59 | license_plate = license_plate.upper()
60 | if _is_valid_old_format(license_plate):
61 | return license_plate[0:3] + "-" + license_plate[3:]
62 | elif _is_valid_mercosul(license_plate):
63 | return license_plate.upper()
64 |
65 | return None
66 |
67 |
68 | # OPERATIONS
69 | ############
70 |
71 |
72 | def is_valid(
73 | license_plate: str, type: Literal["old_format", "mercosul"] | None = None
74 | ) -> bool:
75 | """
76 | Returns if a Brazilian license plate number is valid.
77 | It does not verify if the plate actually exists.
78 |
79 | Args:
80 | license_plate (str): The license plate number to be validated.
81 | type (Literal["old_format", "mercosul"] | None): "old_format" or "mercosul".
82 | If not specified, checks for one or another.
83 | Returns:
84 | bool: True if the plate number is valid. False otherwise.
85 | """
86 |
87 | if type == "old_format":
88 | return _is_valid_old_format(license_plate)
89 | if type == "mercosul":
90 | return _is_valid_mercosul(license_plate)
91 |
92 | return _is_valid_old_format(license_plate) or _is_valid_mercosul(
93 | license_plate
94 | )
95 |
96 |
97 | def remove_symbols(license_plate_number: str) -> str:
98 | """
99 | Removes the dash (-) symbol from a license plate string.
100 |
101 | Args:
102 | license_plate_number (str): A license plate number containing symbols to
103 | be removed.
104 |
105 | Returns:
106 | str | None: The license plate number with the specified symbols removed.
107 |
108 | Example:
109 | >>> remove_symbols("ABC-123")
110 | "ABC123"
111 | >>> remove_symbols("abc123")
112 | "abc123"
113 | >>> remove_symbols("ABCD123")
114 | "ABCD123"
115 | """
116 |
117 | return license_plate_number.replace("-", "")
118 |
119 |
120 | def get_format(license_plate: str) -> str | None:
121 | """
122 | Return the format of a license plate. 'LLLNNNN' for the old pattern and
123 | 'LLLNLNN' for the Mercosul one.
124 |
125 | Args:
126 | license_plate (str): A license plate string without symbols.
127 |
128 | Returns:
129 | str | None: The format of the license plate (LLLNNNN, LLLNLNN) or
130 | 'None' if the format is invalid.
131 |
132 | Example:
133 | >>> get_format("abc123")
134 | "LLLNNNN"
135 | >>> get_format("abc1d23")
136 | "LLLNLNN"
137 | >>> get_format("ABCD123")
138 | None
139 | """
140 |
141 | if _is_valid_old_format(license_plate):
142 | return "LLLNNNN"
143 |
144 | if _is_valid_mercosul(license_plate):
145 | return "LLLNLNN"
146 |
147 | return None
148 |
149 |
150 | def generate(format: str = "LLLNLNN") -> str | None:
151 | """
152 | Generate a valid license plate in the given format. In case no format is
153 | provided, it will return a license plate in the Mercosul format.
154 |
155 | Args:
156 | format (str): The desired format for the license plate.
157 | 'LLLNNNN' for the old pattern or 'LLLNLNN' for the
158 | Mercosul one. Default is 'LLLNLNN'
159 |
160 | Returns:
161 | str | None: A randomly generated license plate number or
162 | 'None' if the format is invalid.
163 |
164 | Example:
165 | >>> generate()
166 | "ABC1D23"
167 | >>> generate(format="LLLNLNN")
168 | "ABC4D56"
169 | >>> generate(format="LLLNNNN")
170 | "ABC123"
171 | >>> generate(format="invalid")
172 | None
173 | """
174 |
175 | generated = ""
176 |
177 | format = format.upper()
178 |
179 | if format not in ("LLLNLNN", "LLLNNNN"):
180 | return None
181 |
182 | for char in format:
183 | if char == "L":
184 | generated += choice(ascii_uppercase)
185 | else:
186 | generated += str(randint(0, 9))
187 |
188 | return generated
189 |
190 |
191 | def _is_valid_old_format(license_plate: str) -> bool:
192 | """
193 | Checks whether a string matches the old format of Brazilian license plate.
194 | Args:
195 | license_plate (str): The desired format for the license plate.
196 | pattern:'LLLNNNN'
197 |
198 | Returns:
199 | bool: True if the plate number is valid. False otherwise.
200 |
201 | """
202 | pattern = re.compile(r"^[A-Za-z]{3}[0-9]{4}$")
203 | return (
204 | isinstance(license_plate, str)
205 | and re.match(pattern, license_plate.strip()) is not None
206 | )
207 |
208 |
209 | def _is_valid_mercosul(license_plate: str) -> bool:
210 | """
211 | Checks whether a string matches the old format of Brazilian license plate.
212 | Args:
213 | license_plate (str): The desired format for the license plate.
214 | pattern:'LLLNLNN'
215 |
216 | Returns:
217 | bool: True if the plate number is valid. False otherwise.
218 |
219 | """
220 | if not isinstance(license_plate, str):
221 | return False
222 |
223 | license_plate = license_plate.upper().strip()
224 | pattern = re.compile(r"^[A-Z]{3}\d[A-Z]\d{2}$")
225 | return re.match(pattern, license_plate) is not None
226 |
--------------------------------------------------------------------------------
/tests/test_date_utils.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from unittest import TestCase
3 | from unittest.mock import patch
4 |
5 | from num2words import num2words
6 |
7 | from brutils import convert_date_to_text
8 | from brutils.data.enums.months import MonthsEnum
9 | from brutils.date_utils import is_holiday
10 |
11 |
12 | class TestIsHoliday(TestCase):
13 | @patch("brutils.date_utils.is_holiday")
14 | def test_feriados_validos(self, mock_is_holiday):
15 | # Testes com feriados válidos
16 | mock_is_holiday.return_value = True
17 | self.assertTrue(is_holiday(datetime(2024, 1, 1))) # Ano Novo
18 | self.assertTrue(
19 | is_holiday(datetime(2024, 7, 9), uf="SP")
20 | ) # Revolução Constitucionalista (SP)
21 | self.assertTrue(
22 | is_holiday(datetime(2024, 9, 7))
23 | ) # Independência do Brasil
24 | self.assertTrue(is_holiday(datetime(2025, 1, 1))) # Ano Novo
25 |
26 | @patch("brutils.date_utils.is_holiday")
27 | def test_dias_normais(self, mock_is_holiday):
28 | # Testes com dias normais
29 | mock_is_holiday.return_value = False
30 | self.assertFalse(is_holiday(datetime(2024, 1, 2))) # Dia normal
31 | self.assertFalse(
32 | is_holiday(datetime(2024, 7, 9), uf="RJ")
33 | ) # Dia normal no RJ
34 |
35 | @patch("brutils.date_utils.is_holiday")
36 | def test_data_invalida(self, mock_is_holiday):
37 | # Testes com data inválida
38 | mock_is_holiday.return_value = None
39 | self.assertIsNone(is_holiday("2024-01-01")) # Formato incorreto
40 | self.assertIsNone(is_holiday(None)) # Data None
41 |
42 | @patch("brutils.date_utils.is_holiday")
43 | def test_uf_invalida(self, mock_is_holiday):
44 | # Testes com UF inválida
45 | mock_is_holiday.return_value = None
46 | self.assertIsNone(
47 | is_holiday(datetime(2024, 1, 1), uf="XX")
48 | ) # UF inválida
49 | self.assertIsNone(
50 | is_holiday(datetime(2024, 1, 1), uf="SS")
51 | ) # UF inválida
52 |
53 | @patch("brutils.date_utils.is_holiday")
54 | def test_limite_de_datas(self, mock_is_holiday):
55 | # Testes com limite de datas
56 | mock_is_holiday.return_value = True
57 | self.assertTrue(is_holiday(datetime(2024, 12, 25))) # Natal
58 | self.assertTrue(
59 | is_holiday(datetime(2024, 11, 15))
60 | ) # Proclamação da República
61 |
62 | @patch("brutils.date_utils.is_holiday")
63 | def test_datas_depois_de_feriados(self, mock_is_holiday):
64 | # Test data after holidays
65 | mock_is_holiday.return_value = False
66 | self.assertFalse(is_holiday(datetime(2024, 12, 26))) # Não é feriado
67 | self.assertFalse(is_holiday(datetime(2025, 1, 2))) # Não é feriado
68 |
69 | @patch("brutils.date_utils.is_holiday")
70 | def test_ano_bissexto(self, mock_is_holiday):
71 | # Teste ano bissexto
72 | mock_is_holiday.return_value = False
73 | self.assertFalse(
74 | is_holiday(datetime(2024, 2, 29))
75 | ) # Não é feriado, mas data válida
76 | # Uncomment to test non-leap year invalid date
77 | # self.assertIsNone(is_holiday(datetime(1900, 2, 29))) # Ano não bissexto, data inválida
78 |
79 | @patch("brutils.date_utils.is_holiday")
80 | def test_data_passada_futura(self, mock_is_holiday):
81 | # Teste de data passada e futura
82 | mock_is_holiday.return_value = True
83 | self.assertTrue(is_holiday(datetime(2023, 1, 1))) # Ano anterior
84 | self.assertTrue(is_holiday(datetime(2100, 12, 25))) # Ano futuro
85 | self.assertFalse(
86 | is_holiday(datetime(2100, 1, 2))
87 | ) # Dia normal em ano futuro
88 |
89 | @patch("brutils.date_utils.is_holiday")
90 | def test_data_sem_uf(self, mock_is_holiday):
91 | # Teste feriado nacional sem UF
92 | mock_is_holiday.return_value = True
93 | self.assertTrue(
94 | is_holiday(datetime(2024, 12, 25))
95 | ) # Natal, feriado nacional
96 | self.assertFalse(
97 | is_holiday(datetime(2024, 7, 9))
98 | ) # Data estadual de SP, sem UF
99 |
100 |
101 | class TestNum2Words(TestCase):
102 | def test_num_conversion(self) -> None:
103 | """
104 | Smoke test of the num2words library.
105 | This test is used to guarantee that our dependency still works.
106 | """
107 | self.assertEqual(num2words(30, lang="pt-br"), "trinta")
108 | self.assertEqual(num2words(42, lang="pt-br"), "quarenta e dois")
109 | self.assertEqual(
110 | num2words(2024, lang="pt-br"), "dois mil e vinte e quatro"
111 | )
112 | self.assertEqual(num2words(0, lang="pt-br"), "zero")
113 | self.assertEqual(num2words(-1, lang="pt-br"), "menos um")
114 |
115 |
116 | class TestConvertDateToText(TestCase):
117 | def test_convert_date_to_text(self):
118 | self.assertEqual(
119 | convert_date_to_text("15/08/2024"),
120 | "Quinze de agosto de dois mil e vinte e quatro",
121 | )
122 | self.assertEqual(
123 | convert_date_to_text("01/01/2000"),
124 | "Primeiro de janeiro de dois mil",
125 | )
126 | self.assertEqual(
127 | convert_date_to_text("31/12/1999"),
128 | "Trinta e um de dezembro de mil novecentos e noventa e nove",
129 | )
130 |
131 | self.assertIsNone(convert_date_to_text("30/02/2020"), None)
132 | self.assertIsNone(convert_date_to_text("30/00/2020"), None)
133 | self.assertIsNone(convert_date_to_text("30/02/2000"), None)
134 | self.assertIsNone(convert_date_to_text("50/09/2000"), None)
135 | self.assertIsNone(convert_date_to_text("25/15/2000"), None)
136 | self.assertIsNone(convert_date_to_text("29/02/2019"), None)
137 |
138 | # Invalid date pattern.
139 | self.assertIsNone(convert_date_to_text("Invalid"))
140 | self.assertIsNone(convert_date_to_text("25/1/2020"))
141 | self.assertIsNone(convert_date_to_text("1924/08/20"))
142 | self.assertIsNone(convert_date_to_text("5/09/2020"))
143 | self.assertIsNone(convert_date_to_text("00/09/2020"))
144 | self.assertIsNone(convert_date_to_text("32/09/2020"))
145 |
146 | self.assertEqual(
147 | convert_date_to_text("29/02/2020"),
148 | "Vinte e nove de fevereiro de dois mil e vinte",
149 | )
150 | self.assertEqual(
151 | convert_date_to_text("01/01/1900"),
152 | "Primeiro de janeiro de mil e novecentos",
153 | )
154 |
155 | months_year = [
156 | (1, "janeiro"),
157 | (2, "fevereiro"),
158 | (3, "marco"),
159 | (4, "abril"),
160 | (5, "maio"),
161 | (6, "junho"),
162 | (7, "julho"),
163 | (8, "agosto"),
164 | (9, "setembro"),
165 | (10, "outubro"),
166 | (11, "novembro"),
167 | (12, "dezembro"),
168 | ]
169 |
170 | def testMonthEnum(self):
171 | for number_month, name_month in self.months_year:
172 | month = MonthsEnum(number_month)
173 | self.assertEqual(month.month_name, name_month)
174 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Added
11 |
12 | - Utilitário `convert_name_to_uf`
13 | - Utilitário `is_valid_legal_nature` [#641](https://github.com/brazilian-utils/python/issues/641)
14 | - Utilitário `get_legal_nature_description` [#641](https://github.com/brazilian-utils/python/issues/641)
15 | - Utilitário `list_all_legal_nature` [#641](https://github.com/brazilian-utils/python/issues/641)
16 | - Utilitário `is_valid_cnh` [#651](https://github.com/brazilian-utils/brutils-python/pull/651)
17 | - Utilitário `is_valid_renavam` [#652](https://github.com/brazilian-utils/brutils-python/pull/652)
18 |
19 | ### Fixed
20 |
21 | - Suporte a anotações modernas no `brutils/cep.py` [#637](https://github.com/brazilian-utils/python/pull/637)
22 |
23 | ### Fixed
24 |
25 | - Suporte a anotações modernas `legal_process` [#622](https://github.com/brazilian-utils/brutils-python/pull/634)
26 |
27 | ### Fixed
28 |
29 | - Suporte a anotações modernas `voter_id` [#623](https://github.com/brazilian-utils/brutils-python/pull/638)
30 |
31 | ## [2.3.0] - 2025-10-07
32 |
33 | ### Added
34 |
35 | - Utilitário `convert_code_to_uf` [#410](https://github.com/brazilian-utils/brutils-python/pull/410)
36 | - Utilitário `is_holiday` [#446](https://github.com/brazilian-utils/brutils-python/pull/446)
37 | - Utilitário `convert_date_to_text`[#415](https://github.com/brazilian-utils/brutils-python/pull/415)
38 | - Utilitário `get_municipality_by_code` [412](https://github.com/brazilian-utils/brutils-python/pull/412)
39 | - Utilitário `get_code_by_municipality_name` [#411](https://github.com/brazilian-utils/brutils-python/pull/411)
40 | - Utilitário `format_currency` [#434](https://github.com/brazilian-utils/brutils-python/pull/434)
41 | - Utilitário `convert_real_to_text` [#525](https://github.com/brazilian-utils/brutils-python/pull/525)
42 | - Utilitário `convert_uf_to_name` [#554](https://github.com/brazilian-utils/brutils-python/pull/554)
43 |
44 | ### Deprecated
45 |
46 | - **BREAKING CHANGES** Suporte ao Python 3.8 [#236](https://github.com/brazilian-utils/brutils-python/pull/561)
47 | - **BREAKING CHANGES** Suporte ao Python 3.9 [#236](https://github.com/brazilian-utils/brutils-python/pull/561)
48 |
49 | ## [2.2.0] - 2024-09-12
50 |
51 | ### Added
52 |
53 | - Utilitário `get_address_from_cep` [#358](https://github.com/brazilian-utils/brutils-python/pull/358)
54 | - Utilitário `get_cep_information_from_address` [#358](https://github.com/brazilian-utils/brutils-python/pull/358)
55 | - Utilitário `format_voter_id` [#363](https://github.com/brazilian-utils/brutils-python/pull/363)
56 | - Utilitário `generate_voter_id` [#220](https://github.com/brazilian-utils/brutils-python/pull/220)
57 |
58 | ## [2.1.1] - 2024-01-06
59 |
60 | ### Fixed
61 |
62 | - `generate_legal_process` [#325](https://github.com/brazilian-utils/brutils-python/pull/325)
63 | - `is_valid_legal_process` [#325](https://github.com/brazilian-utils/brutils-python/pull/325)
64 | - Import do utilitário `convert_license_plate_to_mercosul` [#324](https://github.com/brazilian-utils/brutils-python/pull/324)
65 | - Import do utilitário `generate_license_plate` [#324](https://github.com/brazilian-utils/brutils-python/pull/324)
66 | - Import do utilitário `get_format_license_plate` [#324](https://github.com/brazilian-utils/brutils-python/pull/324)
67 |
68 | ## [2.1.0] - 2024-01-05
69 |
70 | ### Added
71 |
72 | - Suporte ao Python 3.12 [#245](https://github.com/brazilian-utils/brutils-python/pull/245)
73 | - Utilitário `convert_license_plate_to_mercosul` [#226](https://github.com/brazilian-utils/brutils-python/pull/226)
74 | - Utilitário `format_license_plate` [#230](https://github.com/brazilian-utils/brutils-python/pull/230)
75 | - Utilitário `format_phone` [#231](https://github.com/brazilian-utils/brutils-python/pull/231)
76 | - Utilitário `format_pis` [#224](https://github.com/brazilian-utils/brutils-python/pull/224)
77 | - Utilitário `format_legal_process` [#210](https://github.com/brazilian-utils/brutils-python/pull/210)
78 | - Utilitário `generate_license_plate` [#241](https://github.com/brazilian-utils/brutils-python/pull/241)
79 | - Utilitário `generate_phone` [#295](https://github.com/brazilian-utils/brutils-python/pull/295)
80 | - Utilitário `generate_pis` [#218](https://github.com/brazilian-utils/brutils-python/pull/218)
81 | - Utilitário `generate_legal_process` [#208](https://github.com/brazilian-utils/brutils-python/pull/208)
82 | - Utilitário `get_format_license_plate` [#243](https://github.com/brazilian-utils/brutils-python/pull/243)
83 | - Utilitário `is_valid_email` [#213](https://github.com/brazilian-utils/brutils-python/pull/213)
84 | - Utilitário `is_valid_license_plate` [#237](https://github.com/brazilian-utils/brutils-python/pull/237)
85 | - Utilitário `is_valid_phone` [#147](https://github.com/brazilian-utils/brutils-python/pull/147)
86 | - Utilitário `is_valid_pis` [#216](https://github.com/brazilian-utils/brutils-python/pull/216)
87 | - Utilitário `is_valid_legal_process` [#207](https://github.com/brazilian-utils/brutils-python/pull/207)
88 | - Utilitário `is_valid_voter_id` [#235](https://github.com/brazilian-utils/brutils-python/pull/235)
89 | - Utilitário `remove_international_dialing_code` [192](https://github.com/brazilian-utils/brutils-python/pull/192)
90 | - Utilitário `remove_symbols_license_plate` [#182](https://github.com/brazilian-utils/brutils-python/pull/182)
91 | - Utilitário `remove_symbols_phone` [#188](https://github.com/brazilian-utils/brutils-python/pull/188)
92 | - Utilitário `remove_symbols_pis` [#236](https://github.com/brazilian-utils/brutils-python/pull/236)
93 | - Utilitário `remove_symbols_legal_process` [#209](https://github.com/brazilian-utils/brutils-python/pull/209)
94 |
95 | ### Removed
96 |
97 | - **BREAKING CHANGE** Suporte ao Python 3.7 [#236](https://github.com/brazilian-utils/brutils-python/pull/236)
98 |
99 | ## [2.0.0] - 2023-07-23
100 |
101 | ### Added
102 |
103 | - Utilitário `is_valid_cep` [123](https://github.com/brazilian-utils/brutils-python/pull/123)
104 | - Utilitário `format_cep` [125](https://github.com/brazilian-utils/brutils-python/pull/125)
105 | - Utilitário `remove_symbols_cep` [126](https://github.com/brazilian-utils/brutils-python/pull/126)
106 | - Utilitário `generate_cep` [124](https://github.com/brazilian-utils/brutils-python/pull/124)
107 | - Utilitário `is_valid_cpf` [34](https://github.com/brazilian-utils/brutils-python/pull/34)
108 | - Utilitário `format_cpf` [54](https://github.com/brazilian-utils/brutils-python/pull/54)
109 | - Utilitário `remove_symbols_cpf` [57](https://github.com/brazilian-utils/brutils-python/pull/57)
110 | - Utilitário `is_valid_cnpj` [36](https://github.com/brazilian-utils/brutils-python/pull/36)
111 | - Utilitário `format_cnpj` [52](https://github.com/brazilian-utils/brutils-python/pull/52)
112 | - Utilitário `remove_symbols_cnpj` [58](https://github.com/brazilian-utils/brutils-python/pull/58)
113 |
114 | ### Deprecated
115 |
116 | - Utilitário `cpf.sieve`
117 | - Utilitário `cpf.display`
118 | - Utilitário `cpf.validate`
119 | - Utilitário `cnpj.sieve`
120 | - Utilitário `cnpj.display`
121 | - Utilitário `cnpj.validate`
122 |
123 | [Unreleased]: https://github.com/brazilian-utils/brutils-python/compare/v2.3.0...HEAD
124 | [2.3.0]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.3.0
125 | [2.2.0]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.2.0
126 | [2.1.1]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.1.1
127 | [2.1.0]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.1.0
128 | [2.0.0]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.0.0
129 |
--------------------------------------------------------------------------------
/brutils/cnpj.py:
--------------------------------------------------------------------------------
1 | from itertools import chain
2 | from random import randint
3 |
4 | # FORMATTING
5 | ############
6 |
7 |
8 | def sieve(dirty: str) -> str:
9 | """
10 | Removes specific symbols from a CNPJ (Brazilian Company Registration
11 | Number) string.
12 |
13 | This function takes a CNPJ string as input and removes all occurrences of
14 | the '.', '/' and '-' characters from it.
15 |
16 | Args:
17 | cnpj (str): The CNPJ string containing symbols to be removed.
18 |
19 | Returns:
20 | str: A new string with the specified symbols removed.
21 |
22 | Example:
23 | >>> sieve("12.345/6789-01")
24 | "12345678901"
25 | >>> sieve("98/76.543-2101")
26 | "98765432101"
27 |
28 | .. note::
29 | This method should not be used in new code and is only provided for
30 | backward compatibility.
31 | """
32 |
33 | return "".join(filter(lambda char: char not in "./-", dirty))
34 |
35 |
36 | def remove_symbols(dirty: str) -> str:
37 | """
38 | This function is an alias for the `sieve` function, offering a more
39 | descriptive name.
40 |
41 | Args:
42 | dirty (str): The dirty string containing symbols to be removed.
43 |
44 | Returns:
45 | str: A new string with the specified symbols removed.
46 |
47 | Example:
48 | >>> remove_symbols("12.345/6789-01")
49 | "12345678901"
50 | >>> remove_symbols("98/76.543-2101")
51 | "98765432101"
52 | """
53 |
54 | return sieve(dirty)
55 |
56 |
57 | def display(cnpj: str) -> str | None:
58 | """
59 | Will format an adequately formatted numbers-only CNPJ string,
60 | adding in standard formatting visual aid symbols for display.
61 |
62 | Formats a CNPJ (Brazilian Company Registration Number) string for
63 | visual display.
64 |
65 | This function takes a CNPJ string as input, validates its format, and
66 | formats it with standard visual aid symbols for display purposes.
67 |
68 | Args:
69 | cnpj (str): The CNPJ string to be formatted for display.
70 |
71 | Returns:
72 | str: The formatted CNPJ with visual aid symbols if it's valid,
73 | None if it's not valid.
74 |
75 | Example:
76 | >>> display("12345678901234")
77 | "12.345.678/9012-34"
78 | >>> display("98765432100100")
79 | "98.765.432/1001-00"
80 |
81 | .. note::
82 | This method should not be used in new code and is only provided for
83 | backward compatibility.
84 | """
85 |
86 | if not cnpj.isdigit() or len(cnpj) != 14 or len(set(cnpj)) == 1:
87 | return None
88 | return "{}.{}.{}/{}-{}".format(
89 | cnpj[:2], cnpj[2:5], cnpj[5:8], cnpj[8:12], cnpj[12:]
90 | )
91 |
92 |
93 | def format_cnpj(cnpj: str) -> str | None:
94 | """
95 | Formats a CNPJ (Brazilian Company Registration Number) string for visual
96 | display.
97 |
98 | This function takes a CNPJ string as input, validates its format, and
99 | formats it with standard visual aid symbols for display purposes.
100 |
101 | Args:
102 | cnpj (str): The CNPJ string to be formatted for display.
103 |
104 | Returns:
105 | str: The formatted CNPJ with visual aid symbols if it's valid,
106 | None if it's not valid.
107 |
108 | Example:
109 | >>> format_cnpj("03560714000142")
110 | '03.560.714/0001-42'
111 | >>> format_cnpj("98765432100100")
112 | None
113 | """
114 |
115 | if not is_valid(cnpj):
116 | return None
117 |
118 | return "{}.{}.{}/{}-{}".format(
119 | cnpj[:2], cnpj[2:5], cnpj[5:8], cnpj[8:12], cnpj[12:14]
120 | )
121 |
122 |
123 | # OPERATIONS
124 | ############
125 |
126 |
127 | def validate(cnpj: str) -> bool:
128 | """
129 | Validates a CNPJ (Brazilian Company Registration Number) by comparing its
130 | verifying checksum digits to its base number.
131 |
132 | This function checks the validity of a CNPJ by comparing its verifying
133 | checksum digits to its base number. The input should be a string of digits
134 | with the appropriate length.
135 |
136 | Args:
137 | cnpj (str): The CNPJ to be validated.
138 |
139 | Returns:
140 | bool: True if the checksum digits match the base number,
141 | False otherwise.
142 |
143 | Example:
144 | >>> validate("03560714000142")
145 | True
146 | >>> validate("00111222000133")
147 | False
148 |
149 | .. note::
150 | This method should not be used in new code and is only provided for
151 | backward compatibility.
152 | """
153 |
154 | if not cnpj.isdigit() or len(cnpj) != 14 or len(set(cnpj)) == 1:
155 | return False
156 | return all(
157 | _hashdigit(cnpj, i + 13) == int(v) for i, v in enumerate(cnpj[12:])
158 | )
159 |
160 |
161 | def is_valid(cnpj: str) -> bool:
162 | """
163 | Returns whether or not the verifying checksum digits of the given `cnpj`
164 | match its base number.
165 |
166 | This function does not verify the existence of the CNPJ; it only
167 | validates the format of the string.
168 |
169 | Args:
170 | cnpj (str): The CNPJ to be validated, a 14-digit string
171 |
172 | Returns:
173 | bool: True if the checksum digits match the base number,
174 | False otherwise.
175 |
176 | Example:
177 | >>> is_valid("03560714000142")
178 | True
179 | >>> is_valid("00111222000133")
180 | False
181 | """
182 |
183 | return isinstance(cnpj, str) and validate(cnpj)
184 |
185 |
186 | def generate(branch: int = 1) -> str:
187 | """
188 | Generates a random valid CNPJ digit string. An optional branch number
189 | parameter can be given; it defaults to 1.
190 |
191 | Args:
192 | branch (int): An optional branch number to be included in the CNPJ.
193 |
194 | Returns:
195 | str: A randomly generated valid CNPJ string.
196 |
197 | Example:
198 | >>> generate()
199 | "30180536000105"
200 | >>> generate(1234)
201 | "01745284123455"
202 | """
203 |
204 | branch %= 10000
205 | branch += int(branch == 0)
206 | branch = str(branch).zfill(4)
207 | base = str(randint(0, 99999999)).zfill(8) + branch
208 |
209 | return base + _checksum(base)
210 |
211 |
212 | def _hashdigit(cnpj: str, position: int) -> int:
213 | """
214 | Calculates the checksum digit at the given `position` for the provided
215 | `cnpj`. The input must contain all elements before `position`.
216 |
217 | Args:
218 | cnpj (str): The CNPJ for which the checksum digit is calculated.
219 | position (int): The position of the checksum digit to be calculated.
220 |
221 | Returns:
222 | int: The calculated checksum digit.
223 |
224 | Example:
225 | >>> _hashdigit("12345678901234", 13)
226 | 3
227 | >>> _hashdigit("98765432100100", 14)
228 | 9
229 | """
230 |
231 | weightgen = chain(range(position - 8, 1, -1), range(9, 1, -1))
232 | val = (
233 | sum(int(digit) * weight for digit, weight in zip(cnpj, weightgen)) % 11
234 | )
235 | return 0 if val < 2 else 11 - val
236 |
237 |
238 | def _checksum(basenum: str) -> str:
239 | """
240 | Calculates the verifying checksum digits for a given CNPJ base number.
241 |
242 | This function computes the verifying checksum digits for a provided CNPJ
243 | base number. The `basenum` should be a digit-string of the appropriate
244 | length.
245 |
246 | Args:
247 | basenum (str): The base number of the CNPJ for which verifying checksum
248 | digits are calculated.
249 |
250 | Returns:
251 | str: The verifying checksum digits.
252 |
253 | Example:
254 | >>> _checksum("123456789012")
255 | "30"
256 | >>> _checksum("987654321001")
257 | "41"
258 | """
259 |
260 | verifying_digits = str(_hashdigit(basenum, 13))
261 | verifying_digits += str(_hashdigit(basenum + verifying_digits, 14))
262 | return verifying_digits
263 |
--------------------------------------------------------------------------------
/MAINTAINING.md:
--------------------------------------------------------------------------------
1 | # Prompt ChatGPT para Criar Issues
2 |
3 | ## Exemplo: formatar moeda brasileira
4 |
5 | - Criar uma issue para o repositório brutils-python.
6 | - Issue: formatar moeda brasileira
7 | - nome da função: format_brl
8 | - entrada: float
9 | - saída: string formatada
10 | - caso entrada seja inválida, retornar None
11 | - Não implementar a lógica da função. Apenas deixar a docstring e um comentário `# implementar a lógica da função aqui`
12 | - Considerar o maior número de edge cases possíveis
13 | - Criar testes unitários para todos os edge cases
14 | - Estes são os utilitários já existentes na lib:
15 |
16 | ```md
17 | - [CPF](#cpf)
18 | - [is\_valid\_cpf](#is_valid_cpf)
19 | - [format\_cpf](#format_cpf)
20 | - [remove\_symbols\_cpf](#remove_symbols_cpf)
21 | - [generate\_cpf](#generate_cpf)
22 | - [CNPJ](#cnpj)
23 | - [is\_valid\_cnpj](#is_valid_cnpj)
24 | - [format\_cnpj](#format_cnpj)
25 | - [remove\_symbols\_cnpj](#remove_symbols_cnpj)
26 | - [generate\_cnpj](#generate_cnpj)
27 | - [CEP](#cep)
28 | - [is\_valid\_cep](#is_valid_cep)
29 | - [format\_cep](#format_cep)
30 | - [remove\_symbols\_cep](#remove_symbols_cep)
31 | - [generate\_cep](#generate_cep)
32 | - [get\_address\_from\_cep](#get_address_from_cep)
33 | - [get\_cep\_information\_from\_address](#get_cep_information_from_address)
34 | - [Telefone](#telefone)
35 | - [is\_valid\_phone](#is_valid_phone)
36 | - [format\_phone](#format_phone)
37 | - [remove\_symbols\_phone](#remove_symbols_phone)
38 | - [remove\_international\_dialing\_code](#remove_international_dialing_code)
39 | - [generate\_phone](#generate_phone)
40 | - [Email](#email)
41 | - [is\_valid\_email](#is_valid_email)
42 | - [Placa de Carro](#placa-de-carro)
43 | - [is\_valid\_license\_plate](#is_valid_license_plate)
44 | - [format\_license\_plate](#format_license_plate)
45 | - [remove\_symbols\_license\_plate](#remove_symbols_license_plate)
46 | - [generate\_license\_plate](#generate_license_plate)
47 | - [convert\_license\_plate\_to\_mercosul](#convert_license_plate_to_mercosul)
48 | - [get\_format\_license\_plate](#get_format_license_plate)
49 | - [PIS](#pis)
50 | - [is\_valid\_pis](#is_valid_pis)
51 | - [format\_pis](#format_pis)
52 | - [remove\_symbols\_pis](#remove_symbols_pis)
53 | - [generate\_pis](#generate_pis)
54 | - [Processo Jurídico](#processo-jurídico)
55 | - [is\_valid\_legal\_process](#is_valid_legal_process)
56 | - [format\_legal\_process](#format_legal_process)
57 | - [remove\_symbols\_legal\_process](#remove_symbols_legal_process)
58 | - [generate\_legal\_process](#generate_legal_process)
59 | - [Título Eleitoral](#titulo-eleitoral)
60 | - [is_valid_voter_id](#is_valid_voter_id)
61 | - [format_voter_id](#format_voter_id)
62 | - [generate_voter_id](#generate_voter_id)
63 | - [IBGE](#ibge)
64 | - [convert_code_to_uf](#convert_code_to_uf)
65 | ```
66 |
67 | - Seguindo exatamento o mesmo modelo dessa issue:
68 |
69 | ```md
70 | Título da Issue: Conversão de Nome de Estado para UF
71 |
72 | **Seu pedido de recurso está relacionado a um problema? Por favor, descreva.**
73 |
74 | Dado o nome completo de um estado brasileiro, quero obter o código de Unidade Federativa (UF) correspondente. Isso é útil para conversão de nomes completos de estados em siglas utilizadas em sistemas e documentos.
75 |
76 | Por exemplo, converter `"São Paulo"` para `"SP"`.
77 |
78 | **Descreva a solução que você gostaria**
79 |
80 | * Uma função `convert_text_to_uf`, que recebe o nome completo do estado (string) e retorna o código UF correspondente.
81 | * A função deve ignorar maiúsculas e minúsculas, e também deve desconsiderar acentos e o caractere especial ç (considerando c também).
82 | * A função deve verificar se o nome completo é válido e retornar o código UF correspondente.
83 | * Se o nome completo não for válido, a função deve retornar `None`.
84 | * A função deve lidar com todos os estados e o Distrito Federal do Brasil.
85 | * A lista das UFs e seus nomes completos já existe no arquivo `brutils/data/enums/uf.py`. Ela deve ser reutilizada.
86 |
87 | **Descreva alternativas que você considerou**
88 |
89 | 1. Seguir até o passo 8 do [guia de contribuição](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#primeira-contribui%C3%A7%C3%A3o).
90 |
91 | 2. Como parte do passo 8, criar o arquivo: `brutils-python/brutils/ibge/uf.py`.
92 |
93 | ```python
94 | def convert_text_to_uf(state_name): # type: (str) -> str | None
95 | """
96 | Converts a given Brazilian state full name to its corresponding UF code.
97 |
98 | This function takes the full name of a Brazilian state and returns the corresponding
99 | 2-letter UF code. It handles all Brazilian states and the Federal District.
100 |
101 | Args:
102 | state_name (str): The full name of the state to be converted.
103 |
104 | Returns:
105 | str or None: The UF code corresponding to the full state name,
106 | or None if the full state name is invalid.
107 |
108 | Example:
109 | >>> convert_text_to_uf('São Paulo')
110 | "SP"
111 | >>> convert_text_to_uf('Rio de Janeiro')
112 | "RJ"
113 | >>> convert_text_to_uf('Minas Gerais')
114 | "MG"
115 | >>> convert_text_to_uf('Distrito Federal')
116 | "DF"
117 | >>> convert_text_to_uf('Estado Inexistente')
118 | None
119 | """
120 | # implementar a lógica da função aqui
121 | ```
122 |
123 | Importar a nova função no arquivo `brutils-python/brutils/__init__.py`:
124 |
125 | ```python
126 | # UF Imports
127 | from brutils.ibge.uf import (
128 | convert_text_to_uf,
129 | )
130 | ```
131 |
132 | E adicionar o nome da nova função na lista `__all__` do mesmo arquivo `brutils-python/brutils/__init__.py`:
133 |
134 | ```python
135 | __all__ = [
136 | ...
137 | # UF
138 | 'convert_text_to_uf',
139 | ]
140 | ```
141 |
142 | 3. Como parte do passo 9, criar o arquivo de teste: `brutils-python/tests/test_uf.py`.
143 |
144 | ```python
145 | from unittest import TestCase
146 | from brutils.ibge.uf import convert_text_to_uf
147 |
148 | class TestUF(TestCase):
149 | def test_convert_text_to_uf(self):
150 | # Testes para nomes válidos
151 | self.assertEqual(convert_text_to_uf('São Paulo'), "SP")
152 | self.assertEqual(convert_text_to_uf('Rio de Janeiro'), "RJ")
153 | self.assertEqual(convert_text_to_uf('Minas Gerais'), "MG")
154 | self.assertEqual(convert_text_to_uf('Distrito Federal'), "DF")
155 | self.assertEqual(convert_text_to_uf('são paulo'), "SP") # Teste com minúsculas
156 | self.assertEqual(convert_text_to_uf('riO de janeiRo'), "RJ") # Teste com misturas de maiúsculas e minúsculas
157 | self.assertEqual(convert_text_to_uf('minas gerais'), "MG") # Teste com minúsculas
158 | self.assertEqual(convert_text_to_uf('sao paulo'), "SP") # Teste sem acento
159 |
160 | # Testes para nomes inválidos
161 | self.assertIsNone(convert_text_to_uf('Estado Inexistente')) # Nome não existe
162 | self.assertIsNone(convert_text_to_uf('')) # Nome vazio
163 | self.assertIsNone(convert_text_to_uf('123')) # Nome com números
164 | self.assertIsNone(convert_text_to_uf('São Paulo SP')) # Nome com sigla incluída
165 | self.assertIsNone(convert_text_to_uf('A')) # Nome com letra não mapeada
166 | self.assertIsNone(convert_text_to_uf('ZZZ')) # Nome com mais de 2 letras
167 |
168 | # implementar mais casos de teste aqui se necessário
169 | ```
170 |
171 | 4. Seguir os passos seguintes do [guia de contribuição](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#primeira-contribui%C3%A7%C3%A3o).
172 |
173 | **Contexto adicional**
174 |
175 | * A lista de estados e suas siglas é definida pelo Instituto Brasileiro de Geografia e Estatística (IBGE). Para mais detalhes, consulte o [site do IBGE](https://atendimento.tecnospeed.com.br/hc/pt-br/articles/360021494734-Tabela-de-C%C3%B3digo-de-UF-do-IBGE).
176 | * A função deve lidar com a normalização de texto, incluindo a remoção de acentos e a conversão para minúsculas para garantir que o texto seja comparado de forma consistente.
177 | ```
178 |
--------------------------------------------------------------------------------
/brutils/cep.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 | from random import randint
3 | from unicodedata import normalize
4 | from urllib.request import urlopen
5 |
6 | from brutils.data.enums import UF
7 | from brutils.exceptions import CEPNotFound, InvalidCEP
8 | from brutils.schemas import Address
9 |
10 | # FORMATTING
11 | ############
12 |
13 |
14 | def format_cep(cep: str, only_nums=False) -> str | None:
15 | """
16 | Formats a Brazilian CEP (Postal Code) into a standard format.
17 |
18 | This function takes a CEP (Postal Code) as input and,
19 | - Removes special characteres;
20 | - Check if the string follows the CEP length pattern;
21 | - Returns None if the string is out of the pattern;
22 | - Return a string with the formatted CEP.
23 |
24 | Args:
25 | cep (str): The input CEP (Postal Code) to be formatted.
26 |
27 | Returns:
28 | str: The formatted CEP in the "12345-678" format if it's valid,
29 | None if it's not valid.
30 |
31 | Example:
32 | >>> format_cep("12345678")
33 | "12345-678"
34 | >>> format_cep(" 12.345/678 ", only_nums=True)
35 | "12345678"
36 | >>> format_cep("12345")
37 | None
38 | """
39 | ### Checking data type
40 | if not isinstance(cep, str):
41 | return None
42 |
43 | ### Removing special characteres
44 | cep = "".join(filter(str.isalnum, cep))
45 |
46 | ### Checking CEP patterns
47 | if len(cep) != 8:
48 | return None
49 |
50 | ### Returning CEP value
51 | if only_nums:
52 | return cep
53 | else:
54 | return f"{cep[:5]}-{cep[5:]}"
55 |
56 |
57 | # OPERATIONS
58 | ############
59 |
60 |
61 | def is_valid(cep: str) -> bool:
62 | """
63 | Checks if a CEP (Postal Code) is valid.
64 |
65 | To be considered valid, the input must be a string containing exactly 8
66 | digits.
67 | This function does not verify if the CEP is a real postal code; it only
68 | validates the format of the string.
69 |
70 | Args:
71 | cep (str): The string containing the CEP to be checked.
72 |
73 | Returns:
74 | bool: True if the CEP is valid (8 digits), False otherwise.
75 |
76 | Example:
77 | >>> is_valid("12345678")
78 | True
79 | >>> is_valid("12345")
80 | False
81 | >>> is_valid("abcdefgh")
82 | False
83 |
84 | Source:
85 | https://en.wikipedia.org/wiki/Código_de_Endereçamento_Postal
86 | """
87 |
88 | return isinstance(cep, str) and len(cep) == 8 and cep.isdigit()
89 |
90 |
91 | def generate() -> str:
92 | """
93 | Generates a random 8-digit CEP (Postal Code) number as a string.
94 |
95 | Returns:
96 | str: A randomly generated 8-digit number.
97 |
98 | Example:
99 | >>> generate()
100 | "12345678"
101 | """
102 |
103 | generated_number = ""
104 |
105 | for _ in range(8):
106 | generated_number = generated_number + str(randint(0, 9))
107 |
108 | return generated_number
109 |
110 |
111 | # Reference: https://viacep.com.br/
112 | def get_address_from_cep(
113 | cep: str, raise_exceptions: bool = False
114 | ) -> Address | None:
115 | """
116 | Fetches address information from a given CEP (Postal Code) using the ViaCEP API.
117 |
118 | Args:
119 | cep (str): The CEP (Postal Code) to be used in the search.
120 | raise_exceptions (bool, optional): Whether to raise exceptions when the CEP is invalid or not found. Defaults to False.
121 |
122 | Raises:
123 | InvalidCEP: When the input CEP is invalid.
124 | CEPNotFound: When the input CEP is not found.
125 |
126 | Returns:
127 | Address | None: An Address object (TypedDict) containing the address information if the CEP is found, None otherwise.
128 |
129 | Example:
130 | >>> get_address_from_cep("12345678")
131 | {
132 | "cep": "12345-678",
133 | "logradouro": "Rua Example",
134 | "complemento": "",
135 | "bairro": "Example",
136 | "localidade": "Example",
137 | "uf": "EX",
138 | "ibge": "1234567",
139 | "gia": "1234",
140 | "ddd": "12",
141 | "siafi": "1234"
142 | }
143 |
144 | >>> get_address_from_cep("abcdefg")
145 | None
146 |
147 | >>> get_address_from_cep("abcdefg", True)
148 | InvalidCEP: CEP 'abcdefg' is invalid.
149 |
150 | >>> get_address_from_cep("00000000", True)
151 | CEPNotFound: 00000000
152 | """
153 | base_api_url = "https://viacep.com.br/ws/{}/json/"
154 |
155 | clean_cep = format_cep(cep, only_nums=True)
156 | cep_is_valid = is_valid(clean_cep)
157 |
158 | if not cep_is_valid:
159 | if raise_exceptions:
160 | raise InvalidCEP(cep)
161 |
162 | return None
163 |
164 | try:
165 | with urlopen(base_api_url.format(clean_cep)) as f:
166 | response = f.read()
167 | data = loads(response)
168 |
169 | if data.get("erro", False):
170 | raise CEPNotFound(cep)
171 |
172 | return Address(**loads(response))
173 |
174 | except Exception as e:
175 | if raise_exceptions:
176 | raise CEPNotFound(cep) from e
177 |
178 | return None
179 |
180 |
181 | def get_cep_information_from_address(
182 | federal_unit: str, city: str, street: str, raise_exceptions: bool = False
183 | ) -> list[Address] | None:
184 | """
185 | Fetches CEP (Postal Code) options from a given address using the ViaCEP API.
186 |
187 | Args:
188 | federal_unit (str): The two-letter abbreviation of the Brazilian state.
189 | city (str): The name of the city.
190 | street (str): The name (or substring) of the street.
191 | raise_exceptions (bool, optional): Whether to raise exceptions when the address is invalid or not found. Defaults to False.
192 |
193 | Raises:
194 | ValueError: When the input UF is invalid.
195 | CEPNotFound: When the input address is not found.
196 |
197 | Returns:
198 | list[Address] | None: A list of Address objects (TypedDict) containing the address information if the address is found, None otherwise.
199 |
200 | Example:
201 | >>> get_cep_information_from_address("EX", "Example", "Rua Example")
202 | [
203 | {
204 | "cep": "12345-678",
205 | "logradouro": "Rua Example",
206 | "complemento": "",
207 | "bairro": "Example",
208 | "localidade": "Example",
209 | "uf": "EX",
210 | "ibge": "1234567",
211 | "gia": "1234",
212 | "ddd": "12",
213 | "siafi": "1234"
214 | }
215 | ]
216 |
217 | >>> get_cep_information_from_address("A", "Example", "Rua Example")
218 | None
219 |
220 | >>> get_cep_information_from_address("XX", "Example", "Example", True)
221 | ValueError: Invalid UF: XX
222 |
223 | >>> get_cep_information_from_address("SP", "Example", "Example", True)
224 | CEPNotFound: SP - Example - Example
225 | """
226 | if federal_unit in UF.values:
227 | federal_unit = UF(federal_unit).name
228 |
229 | if federal_unit not in UF.names:
230 | if raise_exceptions:
231 | raise ValueError(f"Invalid UF: {federal_unit}")
232 |
233 | return None
234 |
235 | base_api_url = "https://viacep.com.br/ws/{}/{}/{}/json/"
236 |
237 | parsed_city = (
238 | normalize("NFD", city)
239 | .encode("ascii", "ignore")
240 | .decode("utf-8")
241 | .replace(" ", "%20")
242 | )
243 | parsed_street = (
244 | normalize("NFD", street)
245 | .encode("ascii", "ignore")
246 | .decode("utf-8")
247 | .replace(" ", "%20")
248 | )
249 |
250 | try:
251 | with urlopen(
252 | base_api_url.format(federal_unit, parsed_city, parsed_street)
253 | ) as f:
254 | response = f.read()
255 | response = loads(response)
256 |
257 | if len(response) == 0:
258 | raise CEPNotFound(f"{federal_unit} - {city} - {street}")
259 |
260 | return [Address(**address) for address in response]
261 |
262 | except Exception as e:
263 | if raise_exceptions:
264 | raise CEPNotFound(f"{federal_unit} - {city} - {street}") from e
265 |
266 | return None
267 |
--------------------------------------------------------------------------------
/tests/test_cep.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 | from unittest.mock import MagicMock, patch
3 |
4 | from brutils.cep import (
5 | CEPNotFound,
6 | InvalidCEP,
7 | format_cep,
8 | generate,
9 | get_address_from_cep,
10 | get_cep_information_from_address,
11 | is_valid,
12 | )
13 |
14 |
15 | class TestCEP(TestCase):
16 | def test_remove_symbols(self):
17 | self.assertEqual(format_cep("00000000"), "00000-000")
18 | self.assertEqual(format_cep("01310-200", only_nums=True), "01310200")
19 | self.assertEqual(format_cep("01..310.-200.-"), "01310-200")
20 | self.assertEqual(format_cep("abc01310200*!*"), None)
21 | self.assertEqual(format_cep("ab.c1.--.3-102.-0-.0-.*.-!*"), None)
22 |
23 | def test_is_valid(self):
24 | # When CEP is not string, returns False
25 | self.assertIs(is_valid(1), False)
26 |
27 | # When CEP's len is different of 8, returns False
28 | self.assertIs(is_valid("1"), False)
29 |
30 | # When CEP does not contain only digits, returns False
31 | self.assertIs(is_valid("1234567-"), False)
32 |
33 | # When CEP is valid
34 | self.assertIs(is_valid("99999999"), True)
35 | self.assertIs(is_valid("88390000"), True)
36 |
37 | def test_generate(self):
38 | for _ in range(10_000):
39 | self.assertIs(is_valid(generate()), True)
40 |
41 |
42 | @patch("brutils.cep.is_valid")
43 | class TestIsValidToFormat(TestCase):
44 | def test_when_cep_is_valid_returns_True_to_format(self, mock_is_valid):
45 | mock_is_valid.return_value = True
46 |
47 | self.assertEqual(format_cep("01310200"), "01310-200")
48 |
49 | def test_when_cep_is_not_valid_returns_error(self, mock_is_valid):
50 | mock_is_valid.return_value = False
51 |
52 | # When cep isn't valid, returns None
53 | self.assertEqual(format_cep("013102009"), None)
54 |
55 |
56 | @patch("brutils.cep.urlopen")
57 | class TestCEPAPICalls(TestCase):
58 | @patch("brutils.cep.loads")
59 | def test_get_address_from_cep_success(self, mock_loads, mock_urlopen):
60 | mock_loads.return_value = {"cep": "01310-200"}
61 |
62 | self.assertEqual(
63 | get_address_from_cep("01310200", True), {"cep": "01310-200"}
64 | )
65 |
66 | def test_get_address_from_cep_raise_exception_invalid_cep(
67 | self, mock_urlopen
68 | ):
69 | mock_data = MagicMock()
70 | mock_data.read.return_value = {"erro": True}
71 | mock_urlopen.return_value = mock_data
72 |
73 | self.assertIsNone(get_address_from_cep("013102009"))
74 |
75 | def test_get_address_from_cep_invalid_cep_raise_exception_invalid_cep(
76 | self, mock_urlopen
77 | ):
78 | with self.assertRaises(InvalidCEP):
79 | get_address_from_cep("abcdef", True)
80 |
81 | def test_get_address_from_cep_invalid_cep_raise_exception_cep_not_found(
82 | self, mock_urlopen
83 | ):
84 | mock_data = MagicMock()
85 | mock_data.read.return_value = {"erro": True}
86 | mock_urlopen.return_value = mock_data
87 |
88 | with self.assertRaises(CEPNotFound):
89 | get_address_from_cep("01310209", True)
90 |
91 | @patch("brutils.cep.loads")
92 | def test_get_cep_information_from_address_success(
93 | self, mock_loads, mock_urlopen
94 | ):
95 | mock_loads.return_value = [{"cep": "01310-200"}]
96 |
97 | self.assertDictEqual(
98 | get_cep_information_from_address(
99 | "SP", "Example", "Rua Example", True
100 | )[0],
101 | {"cep": "01310-200"},
102 | )
103 |
104 | @patch("brutils.cep.loads")
105 | def test_get_cep_information_from_address_success_with_uf_conversion(
106 | self, mock_loads, mock_urlopen
107 | ):
108 | mock_loads.return_value = [{"cep": "01310-200"}]
109 |
110 | self.assertDictEqual(
111 | get_cep_information_from_address(
112 | "São Paulo", "Example", "Rua Example", True
113 | )[0],
114 | {"cep": "01310-200"},
115 | )
116 |
117 | @patch("brutils.cep.loads")
118 | def test_get_cep_information_from_address_empty_response(
119 | self, mock_loads, mock_urlopen
120 | ):
121 | mock_loads.return_value = []
122 |
123 | self.assertIsNone(
124 | get_cep_information_from_address("SP", "Example", "Rua Example")
125 | )
126 |
127 | @patch("brutils.cep.loads")
128 | def test_get_cep_information_from_address_raise_exception_invalid_cep(
129 | self, mock_loads, mock_urlopen
130 | ):
131 | mock_loads.return_value = {"erro": True}
132 |
133 | self.assertIsNone(
134 | get_cep_information_from_address("SP", "Example", "Rua Example")
135 | )
136 |
137 | def test_get_cep_information_from_address_invalid_cep_dont_raise_exception_invalid_uf(
138 | self, mock_urlopen
139 | ):
140 | self.assertIsNone(
141 | get_cep_information_from_address("ABC", "Example", "Rua Example")
142 | )
143 |
144 | def test_get_cep_information_from_address_invalid_cep_raise_exception_invalid_uf(
145 | self, mock_urlopen
146 | ):
147 | with self.assertRaises(ValueError):
148 | get_cep_information_from_address(
149 | "ABC", "Example", "Rua Example", True
150 | )
151 |
152 | def test_get_cep_information_from_address_invalid_cep_raise_exception_cep_not_found(
153 | self, mock_urlopen
154 | ):
155 | mock_response = MagicMock()
156 | mock_response.read.return_value = {"erro": True}
157 | mock_urlopen.return_value = mock_response
158 |
159 | with self.assertRaises(CEPNotFound):
160 | get_cep_information_from_address(
161 | "SP", "Example", "Rua Example", True
162 | )
163 |
164 |
165 | @patch("brutils.cep.urlopen")
166 | class TestGetAddressFromCEP_MC_DC(TestCase):
167 | def test_ct1_cep_invalido_com_excecao(self, mock_urlopen):
168 | """CT1: cep inválido e raise_exceptions=True → lança InvalidCEP"""
169 | with self.assertRaises(InvalidCEP):
170 | get_address_from_cep("abcdefg", raise_exceptions=True)
171 |
172 | def test_ct2_cep_invalido_sem_excecao(self, mock_urlopen):
173 | """CT2: cep inválido e raise_exceptions=False → retorna None"""
174 | result = get_address_from_cep("abcdefg", raise_exceptions=False)
175 | self.assertIsNone(result)
176 |
177 | @patch("brutils.cep.loads")
178 | def test_ct3_cep_valido(self, mock_loads, mock_urlopen):
179 | """CT3: cep válido → retorna dicionário com endereço"""
180 |
181 | # A função get_address_from_cep faz dois loads() no mesmo JSON
182 | mock_loads.side_effect = [
183 | {"cep": "01001-000"}, # Primeiro loads para verificar "erro"
184 | {"cep": "01001-000"}, # Segundo loads para retornar Address
185 | ]
186 |
187 | # Simula a resposta da API ViaCEP
188 | mock_response = MagicMock()
189 | mock_response.read.return_value = b'{"cep": "01001-000"}'
190 |
191 | # Corrige o mock para suportar o contexto "with urlopen(...) as f"
192 | mock_urlopen.return_value.__enter__.return_value = mock_response
193 |
194 | # Executa a chamada
195 | result = get_address_from_cep("01001000", raise_exceptions=False)
196 |
197 | # Valida o resultado
198 | self.assertIsInstance(result, dict)
199 | self.assertEqual(result.get("cep"), "01001-000")
200 |
201 | def test_ct4_erro_na_api_com_excecao(self, mock_urlopen):
202 | """CT4: cep válido mas inexistente + raise_exceptions=True → lança CEPNotFound"""
203 | mock_response = MagicMock()
204 | mock_response.read.return_value = b'{"erro": true}'
205 | mock_urlopen.return_value = mock_response
206 |
207 | with self.assertRaises(CEPNotFound):
208 | get_address_from_cep("99999999", raise_exceptions=True)
209 |
210 | def test_ct5_erro_na_api_sem_excecao(self, mock_urlopen):
211 | """CT5: cep válido mas inexistente + raise_exceptions=False → retorna None"""
212 | mock_response = mock_urlopen.return_value
213 | mock_response.read.return_value = b'{"erro": true}'
214 |
215 | result = get_address_from_cep("99999999", raise_exceptions=False)
216 | self.assertIsNone(result)
217 |
218 | def test_ct6_viacep_responde_com_erro_e_raise_exceptions_true(
219 | self, mock_urlopen
220 | ):
221 | """
222 | CT6: cobre a linha 172 e a decisão CD2 (data.get("erro", True))
223 | """
224 | mock_response = MagicMock()
225 | mock_response.read.return_value = b'{"erro": true}'
226 | mock_urlopen.return_value = mock_response
227 |
228 | with self.assertRaises(CEPNotFound):
229 | get_address_from_cep("01001000", raise_exceptions=True)
230 |
231 |
232 | if __name__ == "__main__":
233 | main()
234 |
--------------------------------------------------------------------------------