├── .flake8 ├── .githooks └── pre-push ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── nova-funcionalidade-🌟.md │ └── reportar-um-bug-🐛.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── check-lint.yml │ ├── cleanup-inactive-issues.yml │ ├── comment-commands.yml │ ├── publish-to-production.yml │ ├── run-tests.yml │ └── stale.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CODE_OF_CONDUCT_EN.md ├── CONTRIBUTING.md ├── CONTRIBUTING_EN.md ├── CORE_TEAM.md ├── CORE_TEAM_EN.md ├── LICENSE ├── MAINTAINING.md ├── Makefile ├── README.md ├── README_EN.md ├── brutils ├── __init__.py ├── cep.py ├── cnpj.py ├── cpf.py ├── currency.py ├── data │ ├── cities_code.json │ ├── enums │ │ ├── __init__.py │ │ ├── better_enum.py │ │ ├── months.py │ │ └── uf.py │ └── legal_process_ids.json ├── date.py ├── date_utils.py ├── email.py ├── exceptions │ ├── __init__.py │ └── cep.py ├── ibge │ ├── __init__.py │ ├── municipality.py │ └── uf.py ├── legal_process.py ├── license_plate.py ├── phone.py ├── pis.py ├── types │ ├── __init__.py │ └── address.py └── voter_id.py ├── codecov.yml ├── old_versions_documentation ├── README.md └── v1.0.1 │ ├── ENGLISH_VERSION.md │ └── PORTUGUESE_VERSION.md ├── poetry.lock ├── pyproject.toml ├── requirements-dev.txt └── tests ├── __init__.py ├── ibge ├── __init__.py ├── test_municipality.py └── test_uf.py ├── license_plate ├── __init__.py ├── test_is_valid.py └── test_license_plate.py ├── phone ├── __init__.py ├── test_is_valid.py └── test_phone.py ├── test_cep.py ├── test_cnpj.py ├── test_cpf.py ├── test_currency.py ├── test_date.py ├── test_date_utils.py ├── test_email.py ├── test_imports.py ├── test_legal_process.py ├── test_pis.py └── test_voter_id.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=80 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @brazilian-utils/python-maintainers @brazilian-utils/python-core-team 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [cumbucadev, camilamaia] 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/cleanup-inactive-issues.yml: -------------------------------------------------------------------------------- 1 | name: Remove Inactive Issue Assignees 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | cleanup_assignees: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Identify and notify inactive issues 16 | uses: actions/github-script@v7 17 | with: 18 | script: | 19 | const inactiveDays = 60; 20 | const warningDays = 7; 21 | 22 | const issues = await github.paginate(github.rest.issues.listForRepo, { 23 | owner: context.repo.owner, 24 | repo: context.repo.repo, 25 | state: 'open' 26 | }); 27 | 28 | const now = new Date(); 29 | 30 | for (const issue of issues) { 31 | const lastUpdate = new Date(issue.updated_at); 32 | const daysInactive = Math.floor((now - lastUpdate) / (1000 * 60 * 60 * 24)); 33 | 34 | if (daysInactive >= inactiveDays && issue.assignees.length > 0) { 35 | const comments = await github.paginate(github.rest.issues.listComments, { 36 | owner: context.repo.owner, 37 | repo: context.repo.repo, 38 | issue_number: issue.number 39 | }); 40 | 41 | const warningComment = comments.some(comment => 42 | comment.body.includes('[PT-BR] Os assignees serão removidos.') || 43 | comment.body.includes('[EN] The assignees will be removed.') 44 | ); 45 | 46 | if (!warningComment) { 47 | await github.rest.issues.createComment({ 48 | owner: context.repo.owner, 49 | repo: context.repo.repo, 50 | issue_number: issue.number, 51 | body: `:warning: [PT-BR] Esta issue está inativa há ${daysInactive} dias. Os assignees serão removidos em ${warningDays} dias caso não haja atualizações.\n\n:warning: [EN] This issue has been inactive for ${daysInactive} days. The assignees will be removed in ${warningDays} days if there are no updates.` 52 | }); 53 | } 54 | } else if (daysInactive >= inactiveDays + warningDays && issue.assignees.length > 0) { 55 | await github.rest.issues.removeAssignees({ 56 | owner: context.repo.owner, 57 | repo: context.repo.repo, 58 | issue_number: issue.number, 59 | assignees: issue.assignees.map(assignee => assignee.login) 60 | }); 61 | 62 | await github.rest.issues.createComment({ 63 | owner: context.repo.owner, 64 | repo: context.repo.repo, 65 | issue_number: issue.number, 66 | body: `✅ [PT-BR] Os assignees foram removidos devido à inatividade prolongada da issue.\n\n✅ [EN] The assignees have been removed due to prolonged inactivity of this issue.` 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/comment-commands.yml: -------------------------------------------------------------------------------- 1 | name: Comandos nos comentários 2 | on: 3 | issue_comment: 4 | types: created 5 | 6 | permissions: 7 | contents: read 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | issue_assign: 13 | runs-on: ubuntu-22.04 14 | if: (!github.event.issue.pull_request) && github.event.comment.body == 'bora!' 15 | concurrency: 16 | group: ${{ github.actor }}-issue-assign 17 | steps: 18 | - run: | 19 | echo "Issue ${{ github.event.issue.number }} atribuida a ${{ github.event.comment.user.login }}" 20 | echo "Verifique [o guia de contribuição](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md) para mais informações sobre como submeter sua Pull Request." 21 | curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -d '{"assignees": ["${{ github.event.comment.user.login }}"]}' https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/assignees 22 | 23 | - name: Create or Update Comment 24 | uses: peter-evans/create-or-update-comment@v4.0.0 25 | with: 26 | issue-number: ${{ github.event.issue.number }} 27 | body: | 28 | Issue ${{ github.event.issue.number }} atribuida a ${{ github.event.comment.user.login }} :rocket:" 29 | "Verifique [o guia de contribuição](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md) para mais informações sobre como submeter sua Pull Request." 30 | -------------------------------------------------------------------------------- /.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/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.8', '3.9', '3.10', '3.11', '3.12'] 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/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Mark stale pull requests' 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | days-before-stale: 60 19 | days-before-close: 7 20 | stale-pr-label: "Stale" 21 | stale-pr-message: > 22 | [PT-BR] Este PR está inativo há muito tempo. Caso não seja atualizado, será fechado em 7 dias. 23 | [EN] This PR has been inactive for a long time. If no updates are made, it will be closed in 7 days. 24 | close-pr-message: > 25 | [PT-BR] Este PR foi fechado devido à inatividade. Caso ainda seja relevante, por favor, reabra ou crie um novo PR. 26 | [EN] This PR has been closed due to inactivity. If it is still relevant, please reopen or create a new PR. 27 | exempt-issue-labels: "keep-open" 28 | operations-per-run: 30 29 | remove-stale-when-updated: true -------------------------------------------------------------------------------- /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 | ### Added 10 | 11 | - Utilitário `convert_code_to_uf` [#397](https://github.com/brazilian-utils/brutils-python/pull/410) 12 | - Utilitário `is_holiday` [#446](https://github.com/brazilian-utils/brutils-python/pull/446) 13 | - Utilitário `convert_date_to_text`[#394](https://github.com/brazilian-utils/brutils-python/pull/415) 14 | - Utilitário `get_municipality_by_code` [412](https://github.com/brazilian-utils/brutils-python/pull/412) 15 | - Utilitário `get_code_by_municipality_name` [#399](https://github.com/brazilian-utils/brutils-python/issues/399) 16 | - Utilitário `format_currency` [#426](https://github.com/brazilian-utils/brutils-python/issues/426) 17 | - Utilitário `convert_real_to_text` [#387](https://github.com/brazilian-utils/brutils-python/pull/525) 18 | 19 | ## [2.2.0] - 2024-09-12 20 | 21 | ### Added 22 | 23 | - Utilitário `get_address_from_cep` [#358](https://github.com/brazilian-utils/brutils-python/pull/358) 24 | - Utilitário `get_cep_information_from_address` [#358](https://github.com/brazilian-utils/brutils-python/pull/358) 25 | - Utilitário `format_voter_id` [#221](https://github.com/brazilian-utils/brutils-python/issues/221) 26 | - Utilitário `generate_voter_id` [#220](https://github.com/brazilian-utils/brutils-python/pull/220) 27 | 28 | ## [2.1.1] - 2024-01-06 29 | 30 | ### Fixed 31 | 32 | - `generate_legal_process` [#325](https://github.com/brazilian-utils/brutils-python/pull/325) 33 | - `is_valid_legal_process` [#325](https://github.com/brazilian-utils/brutils-python/pull/325) 34 | - Import do utilitário `convert_license_plate_to_mercosul` [#324](https://github.com/brazilian-utils/brutils-python/pull/324) 35 | - Import do utilitário `generate_license_plate` [#324](https://github.com/brazilian-utils/brutils-python/pull/324) 36 | - Import do utilitário `get_format_license_plate` [#324](https://github.com/brazilian-utils/brutils-python/pull/324) 37 | 38 | ## [2.1.0] - 2024-01-05 39 | 40 | ### Added 41 | 42 | - Suporte ao Python 3.12 [#245](https://github.com/brazilian-utils/brutils-python/pull/245) 43 | - Utilitário `convert_license_plate_to_mercosul` [#226](https://github.com/brazilian-utils/brutils-python/pull/226) 44 | - Utilitário `format_license_plate` [#230](https://github.com/brazilian-utils/brutils-python/pull/230) 45 | - Utilitário `format_phone` [#231](https://github.com/brazilian-utils/brutils-python/pull/231) 46 | - Utilitário `format_pis` [#224](https://github.com/brazilian-utils/brutils-python/pull/224) 47 | - Utilitário `format_legal_process` [#210](https://github.com/brazilian-utils/brutils-python/pull/210) 48 | - Utilitário `generate_license_plate` [#241](https://github.com/brazilian-utils/brutils-python/pull/241) 49 | - Utilitário `generate_phone` [#295](https://github.com/brazilian-utils/brutils-python/pull/295) 50 | - Utilitário `generate_pis` [#218](https://github.com/brazilian-utils/brutils-python/pull/218) 51 | - Utilitário `generate_legal_process` [#208](https://github.com/brazilian-utils/brutils-python/pull/208) 52 | - Utilitário `get_format_license_plate` [#243](https://github.com/brazilian-utils/brutils-python/pull/243) 53 | - Utilitário `is_valid_email` [#213](https://github.com/brazilian-utils/brutils-python/pull/213) 54 | - Utilitário `is_valid_license_plate` [#237](https://github.com/brazilian-utils/brutils-python/pull/237) 55 | - Utilitário `is_valid_phone` [#147](https://github.com/brazilian-utils/brutils-python/pull/147) 56 | - Utilitário `is_valid_pis` [#216](https://github.com/brazilian-utils/brutils-python/pull/216) 57 | - Utilitário `is_valid_legal_process` [#207](https://github.com/brazilian-utils/brutils-python/pull/207) 58 | - Utilitário `is_valid_voter_id` [#235](https://github.com/brazilian-utils/brutils-python/pull/235) 59 | - Utilitário `remove_international_dialing_code` [192](https://github.com/brazilian-utils/brutils-python/pull/192) 60 | - Utilitário `remove_symbols_license_plate` [#182](https://github.com/brazilian-utils/brutils-python/pull/182) 61 | - Utilitário `remove_symbols_phone` [#188](https://github.com/brazilian-utils/brutils-python/pull/188) 62 | - Utilitário `remove_symbols_pis` [#236](https://github.com/brazilian-utils/brutils-python/pull/236) 63 | - Utilitário `remove_symbols_legal_process` [#209](https://github.com/brazilian-utils/brutils-python/pull/209) 64 | 65 | ### Removed 66 | 67 | - Suporte ao Python 3.7 [#236](https://github.com/brazilian-utils/brutils-python/pull/236) 68 | 69 | ## [2.0.0] - 2023-07-23 70 | 71 | ### Added 72 | 73 | - Utilitário `is_valid_cep` [123](https://github.com/brazilian-utils/brutils-python/pull/123) 74 | - Utilitário `format_cep` [125](https://github.com/brazilian-utils/brutils-python/pull/125) 75 | - Utilitário `remove_symbols_cep` [126](https://github.com/brazilian-utils/brutils-python/pull/126) 76 | - Utilitário `generate_cep` [124](https://github.com/brazilian-utils/brutils-python/pull/124) 77 | - Utilitário `is_valid_cpf` [34](https://github.com/brazilian-utils/brutils-python/pull/34) 78 | - Utilitário `format_cpf` [54](https://github.com/brazilian-utils/brutils-python/pull/54) 79 | - Utilitário `remove_symbols_cpf` [57](https://github.com/brazilian-utils/brutils-python/pull/57) 80 | - Utilitário `is_valid_cnpj` [36](https://github.com/brazilian-utils/brutils-python/pull/36) 81 | - Utilitário `format_cnpj` [52](https://github.com/brazilian-utils/brutils-python/pull/52) 82 | - Utilitário `remove_symbols_cnpj` [58](https://github.com/brazilian-utils/brutils-python/pull/58) 83 | 84 | ### Deprecated 85 | 86 | - Utilitário `cpf.sieve` 87 | - Utilitário `cpf.display` 88 | - Utilitário `cpf.validate` 89 | - Utilitário `cnpj.sieve` 90 | - Utilitário `cnpj.display` 91 | - Utilitário `cnpj.validate` 92 | 93 | [Unreleased]: https://github.com/brazilian-utils/brutils-python/compare/v2.2.0...HEAD 94 | [2.2.0]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.2.0 95 | [2.1.1]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.1.1 96 | [2.1.0]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.1.0 97 | [2.0.0]: https://github.com/brazilian-utils/brutils-python/releases/tag/v2.0.0 98 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | @PYTHONDONTWRITEBYTECODE=1 poetry run python3 -m unittest discover tests/ -v 26 | -------------------------------------------------------------------------------- /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 | from brutils.cep import remove_symbols as remove_symbols_cep 10 | 11 | # CNPJ Imports 12 | from brutils.cnpj import format_cnpj 13 | from brutils.cnpj import generate as generate_cnpj 14 | from brutils.cnpj import is_valid as is_valid_cnpj 15 | from brutils.cnpj import remove_symbols as remove_symbols_cnpj 16 | 17 | # CPF Imports 18 | from brutils.cpf import format_cpf 19 | from brutils.cpf import generate as generate_cpf 20 | from brutils.cpf import is_valid as is_valid_cpf 21 | from brutils.cpf import remove_symbols as remove_symbols_cpf 22 | 23 | # Currency 24 | from brutils.currency import convert_real_to_text, format_currency 25 | 26 | # Date imports 27 | from brutils.date import convert_date_to_text 28 | 29 | # Date Utils Import 30 | from brutils.date_utils import is_holiday 31 | 32 | # Email Import 33 | from brutils.email import is_valid as is_valid_email 34 | 35 | # IBGE Imports 36 | from brutils.ibge.municipality import ( 37 | get_code_by_municipality_name, 38 | get_municipality_by_code, 39 | ) 40 | from brutils.ibge.uf import convert_code_to_uf 41 | 42 | # Legal Process Imports 43 | from brutils.legal_process import format_legal_process 44 | from brutils.legal_process import generate as generate_legal_process 45 | from brutils.legal_process import is_valid as is_valid_legal_process 46 | from brutils.legal_process import remove_symbols as remove_symbols_legal_process 47 | 48 | # License Plate Imports 49 | from brutils.license_plate import ( 50 | convert_to_mercosul as convert_license_plate_to_mercosul, 51 | ) 52 | from brutils.license_plate import format_license_plate 53 | from brutils.license_plate import generate as generate_license_plate 54 | from brutils.license_plate import get_format as get_format_license_plate 55 | from brutils.license_plate import is_valid as is_valid_license_plate 56 | from brutils.license_plate import remove_symbols as remove_symbols_license_plate 57 | 58 | # Phone Imports 59 | from brutils.phone import ( 60 | format_phone, 61 | remove_international_dialing_code, 62 | remove_symbols_phone, 63 | ) 64 | from brutils.phone import generate as generate_phone 65 | from brutils.phone import is_valid as is_valid_phone 66 | 67 | # PIS Imports 68 | from brutils.pis import format_pis 69 | from brutils.pis import generate as generate_pis 70 | from brutils.pis import is_valid as is_valid_pis 71 | from brutils.pis import remove_symbols as remove_symbols_pis 72 | 73 | # Voter ID Imports 74 | from brutils.voter_id import format_voter_id 75 | from brutils.voter_id import generate as generate_voter_id 76 | from brutils.voter_id import is_valid as is_valid_voter_id 77 | 78 | # Defining __all__ to expose the public methods 79 | __all__ = [ 80 | # CEP 81 | "format_cep", 82 | "get_address_from_cep", 83 | "get_cep_information_from_address", 84 | "generate_cep", 85 | "is_valid_cep", 86 | "remove_symbols_cep", 87 | # CNPJ 88 | "format_cnpj", 89 | "generate_cnpj", 90 | "is_valid_cnpj", 91 | "remove_symbols_cnpj", 92 | # CPF 93 | "format_cpf", 94 | "generate_cpf", 95 | "is_valid_cpf", 96 | "remove_symbols_cpf", 97 | # Date 98 | "convert_date_to_text", 99 | # Email 100 | "is_valid_email", 101 | # Legal Process 102 | "format_legal_process", 103 | "generate_legal_process", 104 | "is_valid_legal_process", 105 | "remove_symbols_legal_process", 106 | # License Plate 107 | "convert_license_plate_to_mercosul", 108 | "format_license_plate", 109 | "generate_license_plate", 110 | "get_format_license_plate", 111 | "is_valid_license_plate", 112 | "remove_symbols_license_plate", 113 | # Phone 114 | "format_phone", 115 | "remove_international_dialing_code", 116 | "remove_symbols_phone", 117 | "generate_phone", 118 | "is_valid_phone", 119 | # PIS 120 | "format_pis", 121 | "generate_pis", 122 | "is_valid_pis", 123 | "remove_symbols_pis", 124 | # Voter ID 125 | "format_voter_id", 126 | "generate_voter_id", 127 | "is_valid_voter_id", 128 | # IBGE 129 | "convert_code_to_uf", 130 | "get_municipality_by_code", 131 | "get_code_by_municipality_name", 132 | # Date Utils 133 | "is_holiday", 134 | # Currency 135 | "format_currency", 136 | "convert_real_to_text", 137 | ] 138 | -------------------------------------------------------------------------------- /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.types import Address 9 | 10 | # FORMATTING 11 | ############ 12 | 13 | 14 | def remove_symbols(dirty): # type: (str) -> str 15 | """ 16 | Removes specific symbols from a given CEP (Postal Code). 17 | 18 | This function takes a CEP (Postal Code) as input and removes all occurrences 19 | of the '.' and '-' characters from it. 20 | 21 | Args: 22 | cep (str): The input CEP (Postal Code) containing symbols to be removed. 23 | 24 | Returns: 25 | str: A new string with the specified symbols removed. 26 | 27 | Example: 28 | >>> remove_symbols("123-45.678.9") 29 | "123456789" 30 | >>> remove_symbols("abc.xyz") 31 | "abcxyz" 32 | """ 33 | 34 | return "".join(filter(lambda char: char not in ".-", dirty)) 35 | 36 | 37 | def format_cep(cep): # type: (str) -> str | None 38 | """ 39 | Formats a Brazilian CEP (Postal Code) into a standard format. 40 | 41 | This function takes a CEP (Postal Code) as input and, if it is a valid 42 | 8-digit CEP, formats it into the standard "12345-678" format. 43 | 44 | Args: 45 | cep (str): The input CEP (Postal Code) to be formatted. 46 | 47 | Returns: 48 | str: The formatted CEP in the "12345-678" format if it's valid, 49 | None if it's not valid. 50 | 51 | Example: 52 | >>> format_cep("12345678") 53 | "12345-678" 54 | >>> format_cep("12345") 55 | None 56 | """ 57 | 58 | return f"{cep[:5]}-{cep[5:8]}" if is_valid(cep) else None 59 | 60 | 61 | # OPERATIONS 62 | ############ 63 | 64 | 65 | def is_valid(cep): # type: (str) -> bool 66 | """ 67 | Checks if a CEP (Postal Code) is valid. 68 | 69 | To be considered valid, the input must be a string containing exactly 8 70 | digits. 71 | This function does not verify if the CEP is a real postal code; it only 72 | validates the format of the string. 73 | 74 | Args: 75 | cep (str): The string containing the CEP to be checked. 76 | 77 | Returns: 78 | bool: True if the CEP is valid (8 digits), False otherwise. 79 | 80 | Example: 81 | >>> is_valid("12345678") 82 | True 83 | >>> is_valid("12345") 84 | False 85 | >>> is_valid("abcdefgh") 86 | False 87 | 88 | Source: 89 | https://en.wikipedia.org/wiki/Código_de_Endereçamento_Postal 90 | """ 91 | 92 | return isinstance(cep, str) and len(cep) == 8 and cep.isdigit() 93 | 94 | 95 | def generate(): # type: () -> str 96 | """ 97 | Generates a random 8-digit CEP (Postal Code) number as a string. 98 | 99 | Returns: 100 | str: A randomly generated 8-digit number. 101 | 102 | Example: 103 | >>> generate() 104 | "12345678" 105 | """ 106 | 107 | generated_number = "" 108 | 109 | for _ in range(8): 110 | generated_number = generated_number + str(randint(0, 9)) 111 | 112 | return generated_number 113 | 114 | 115 | # Reference: https://viacep.com.br/ 116 | def get_address_from_cep(cep, raise_exceptions=False): # type: (str, bool) -> Address | None 117 | """ 118 | Fetches address information from a given CEP (Postal Code) using the ViaCEP API. 119 | 120 | Args: 121 | cep (str): The CEP (Postal Code) to be used in the search. 122 | raise_exceptions (bool, optional): Whether to raise exceptions when the CEP is invalid or not found. Defaults to False. 123 | 124 | Raises: 125 | InvalidCEP: When the input CEP is invalid. 126 | CEPNotFound: When the input CEP is not found. 127 | 128 | Returns: 129 | Address | None: An Address object (TypedDict) containing the address information if the CEP is found, None otherwise. 130 | 131 | Example: 132 | >>> get_address_from_cep("12345678") 133 | { 134 | "cep": "12345-678", 135 | "logradouro": "Rua Example", 136 | "complemento": "", 137 | "bairro": "Example", 138 | "localidade": "Example", 139 | "uf": "EX", 140 | "ibge": "1234567", 141 | "gia": "1234", 142 | "ddd": "12", 143 | "siafi": "1234" 144 | } 145 | 146 | >>> get_address_from_cep("abcdefg") 147 | None 148 | 149 | >>> get_address_from_cep("abcdefg", True) 150 | InvalidCEP: CEP 'abcdefg' is invalid. 151 | 152 | >>> get_address_from_cep("00000000", True) 153 | CEPNotFound: 00000000 154 | """ 155 | base_api_url = "https://viacep.com.br/ws/{}/json/" 156 | 157 | clean_cep = remove_symbols(cep) 158 | cep_is_valid = is_valid(clean_cep) 159 | 160 | if not cep_is_valid: 161 | if raise_exceptions: 162 | raise InvalidCEP(cep) 163 | 164 | return None 165 | 166 | try: 167 | with urlopen(base_api_url.format(clean_cep)) as f: 168 | response = f.read() 169 | data = loads(response) 170 | 171 | if data.get("erro", False): 172 | raise CEPNotFound(cep) 173 | 174 | return Address(**loads(response)) 175 | 176 | except Exception as e: 177 | if raise_exceptions: 178 | raise CEPNotFound(cep) from e 179 | 180 | return None 181 | 182 | 183 | def get_cep_information_from_address( 184 | federal_unit, city, street, raise_exceptions=False 185 | ): # type: (str, str, str, bool) -> list[Address] | None 186 | """ 187 | Fetches CEP (Postal Code) options from a given address using the ViaCEP API. 188 | 189 | Args: 190 | federal_unit (str): The two-letter abbreviation of the Brazilian state. 191 | city (str): The name of the city. 192 | street (str): The name (or substring) of the street. 193 | raise_exceptions (bool, optional): Whether to raise exceptions when the address is invalid or not found. Defaults to False. 194 | 195 | Raises: 196 | ValueError: When the input UF is invalid. 197 | CEPNotFound: When the input address is not found. 198 | 199 | Returns: 200 | list[Address] | None: A list of Address objects (TypedDict) containing the address information if the address is found, None otherwise. 201 | 202 | Example: 203 | >>> get_cep_information_from_address("EX", "Example", "Rua Example") 204 | [ 205 | { 206 | "cep": "12345-678", 207 | "logradouro": "Rua Example", 208 | "complemento": "", 209 | "bairro": "Example", 210 | "localidade": "Example", 211 | "uf": "EX", 212 | "ibge": "1234567", 213 | "gia": "1234", 214 | "ddd": "12", 215 | "siafi": "1234" 216 | } 217 | ] 218 | 219 | >>> get_cep_information_from_address("A", "Example", "Rua Example") 220 | None 221 | 222 | >>> get_cep_information_from_address("XX", "Example", "Example", True) 223 | ValueError: Invalid UF: XX 224 | 225 | >>> get_cep_information_from_address("SP", "Example", "Example", True) 226 | CEPNotFound: SP - Example - Example 227 | """ 228 | if federal_unit in UF.values: 229 | federal_unit = UF(federal_unit).name 230 | 231 | if federal_unit not in UF.names: 232 | if raise_exceptions: 233 | raise ValueError(f"Invalid UF: {federal_unit}") 234 | 235 | return None 236 | 237 | base_api_url = "https://viacep.com.br/ws/{}/{}/{}/json/" 238 | 239 | parsed_city = ( 240 | normalize("NFD", city) 241 | .encode("ascii", "ignore") 242 | .decode("utf-8") 243 | .replace(" ", "%20") 244 | ) 245 | parsed_street = ( 246 | normalize("NFD", street) 247 | .encode("ascii", "ignore") 248 | .decode("utf-8") 249 | .replace(" ", "%20") 250 | ) 251 | 252 | try: 253 | with urlopen( 254 | base_api_url.format(federal_unit, parsed_city, parsed_street) 255 | ) as f: 256 | response = f.read() 257 | response = loads(response) 258 | 259 | if len(response) == 0: 260 | raise CEPNotFound(f"{federal_unit} - {city} - {street}") 261 | 262 | return [Address(**address) for address in response] 263 | 264 | except Exception as e: 265 | if raise_exceptions: 266 | raise CEPNotFound(f"{federal_unit} - {city} - {street}") from e 267 | 268 | return None 269 | -------------------------------------------------------------------------------- /brutils/cnpj.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from random import randint 3 | 4 | # FORMATTING 5 | ############ 6 | 7 | 8 | def sieve(dirty): # type: (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): # type: (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): # type: (str) -> str 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): # type: (str) -> str 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): # type: (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): # type: (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=1): # type: (int) -> 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, position): # type: (str, 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): # type: (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 | -------------------------------------------------------------------------------- /brutils/cpf.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | # FORMATTING 4 | ############ 5 | 6 | 7 | def sieve(dirty): # type: (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): # type: (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): # type: (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): # type: (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): # type: (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): # type: (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(): # type: () -> 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, position): # type: (str, 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): # type: (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/currency.py: -------------------------------------------------------------------------------- 1 | from decimal import ROUND_DOWN, Decimal, InvalidOperation 2 | from typing import Union 3 | 4 | from num2words import num2words 5 | 6 | 7 | def format_currency(value): # type: (float) -> str | None 8 | """ 9 | Formats a given float as Brazilian currency (R$). 10 | 11 | This function takes a float value and returns a formatted string representing 12 | the value as Brazilian currency, using the correct thousand and decimal separators. 13 | 14 | Args: 15 | value (float or Decimal): The numeric value to be formatted. 16 | 17 | Returns: 18 | str or None: A string formatted as Brazilian currency, or None if the input is invalid. 19 | 20 | Example: 21 | >>> format_currency(1234.56) 22 | "R$ 1.234,56" 23 | >>> format_currency(0) 24 | "R$ 0,00" 25 | >>> format_currency(-9876.54) 26 | "R$ -9.876,54" 27 | >>> format_currency(None) 28 | None 29 | >>> format_currency("not a number") 30 | None 31 | """ 32 | 33 | try: 34 | decimal_value = Decimal(value) 35 | formatted_value = ( 36 | f"R$ {decimal_value:,.2f}".replace(",", "_") 37 | .replace(".", ",") 38 | .replace("_", ".") 39 | ) 40 | 41 | return formatted_value 42 | except InvalidOperation: 43 | return None 44 | 45 | 46 | def convert_real_to_text(amount: Decimal) -> Union[str, None]: 47 | """ 48 | Converts a given monetary value in Brazilian Reais to its textual representation. 49 | 50 | This function takes a decimal number representing a monetary value in Reais 51 | and converts it to a string with the amount written out in Brazilian Portuguese. It 52 | handles both the integer part (Reais) and the fractional part (centavos), respecting 53 | the correct grammar for singular and plural cases, as well as special cases like zero 54 | and negative values. 55 | 56 | Args: 57 | amount (decimal): The monetary value to be converted into text. 58 | - The integer part represents Reais. 59 | - The decimal part represents centavos. 60 | - 2 decimal places 61 | 62 | Returns: 63 | str: A string with the monetary value written out in Brazilian Portuguese. 64 | - Returns "Zero reais" for a value of 0.00. 65 | - Returns None if the amount is invalid or absolutely greater than 1 quadrillion. 66 | - Handles negative values, adding "Menos" at the beginning of the string. 67 | 68 | Limitations: 69 | - This function may lose precision by ±1 cent for cases where the absolute value 70 | is beyond trillions due to floating-point rounding errors. 71 | 72 | Example: 73 | >>> convert_real_to_text(1523.45) 74 | "Mil, quinhentos e vinte e três reais e quarenta e cinco centavos" 75 | >>> convert_real_to_text(1.00) 76 | "Um real" 77 | >>> convert_real_to_text(0.50) 78 | "Cinquenta centavos" 79 | >>> convert_real_to_text(0.00) 80 | "Zero reais" 81 | >>> convert_real_to_text(-50.25) 82 | "Menos cinquenta reais e vinte e cinco centavos" 83 | """ 84 | 85 | try: 86 | amount = Decimal(str(amount)).quantize( 87 | Decimal("0.01"), rounding=ROUND_DOWN 88 | ) 89 | except InvalidOperation: 90 | return None 91 | 92 | if amount.is_nan() or amount.is_infinite(): 93 | return None 94 | 95 | if abs(amount) > Decimal("1000000000000000.00"): # 1 quadrillion 96 | return None 97 | 98 | negative = amount < 0 99 | amount = abs(amount) 100 | 101 | reais = int(amount) 102 | centavos = int((amount - reais) * 100) 103 | 104 | parts = [] 105 | 106 | if reais > 0: 107 | """ 108 | Note: 109 | Although the `num2words` library provides a "to='currency'" feature, it has known 110 | issues with the representation of "zero reais" and "zero centavos". Therefore, this 111 | implementation uses only the traditional number-to-text conversion for better accuracy. 112 | """ 113 | reais_text = num2words(reais, lang="pt_BR") 114 | currency_text = "real" if reais == 1 else "reais" 115 | conector = "de " if reais_text.endswith(("lhão", "lhões")) else "" 116 | parts.append(f"{reais_text} {conector}{currency_text}") 117 | 118 | if centavos > 0: 119 | centavos_text = f"{num2words(centavos, lang='pt_BR')} {'centavo' if centavos == 1 else 'centavos'}" 120 | if reais > 0: 121 | parts.append(f"e {centavos_text}") 122 | else: 123 | parts.append(centavos_text) 124 | 125 | if reais == 0 and centavos == 0: 126 | parts.append("Zero reais") 127 | 128 | result = " ".join(parts) 129 | if negative: 130 | result = f"Menos {result}" 131 | 132 | return result.capitalize() 133 | -------------------------------------------------------------------------------- /brutils/data/enums/__init__.py: -------------------------------------------------------------------------------- 1 | from .uf import CODE_TO_UF, UF 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/data/enums/uf.py: -------------------------------------------------------------------------------- 1 | from .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 CODE_TO_UF(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 = "52" 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 | -------------------------------------------------------------------------------- /brutils/data/legal_process_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgao_1": { "id_tribunal": [1], "id_foro": [0] }, 3 | "orgao_2": { "id_tribunal": [2], "id_foro": [0] }, 4 | "orgao_3": { "id_tribunal": [3], "id_foro": [0] }, 5 | "orgao_4": { 6 | "id_tribunal": [1, 2, 3, 4, 5], 7 | "id_foro": [ 8 | 5120, 4100, 4101, 8200, 8201, 8202, 3600, 3601, 3602, 3603, 3100, 7200, 9 | 7201, 7202, 7203, 6181, 6182, 6183, 7205, 7206, 7207, 7208, 7209, 7211, 10 | 7213, 7214, 7215, 7216, 6201, 5190, 9810, 9305, 6126, 9830, 4200, 6127, 11 | 8300, 8302, 8303, 8304, 9330, 3700, 3701, 3702, 8308, 3200, 3201, 7310, 12 | 6301, 6302, 6303, 6304, 6305, 6306, 6307, 9380, 6308, 6309, 6310, 6311, 13 | 6312, 6313, 6314, 6315, 6316, 6317, 6318, 6319, 6320, 4300, 8400, 8401, 14 | 8402, 9710, 3800, 3801, 3802, 3803, 3804, 3805, 3806, 3807, 3808, 3809, 15 | 3810, 3811, 3300, 3301, 3302, 3303, 3304, 3305, 3306, 3307, 3308, 3309, 16 | 3310, 3311, 3812, 3813, 3814, 3815, 6401, 9720, 8500, 8501, 8502, 3900, 17 | 3901, 3902, 3903, 3904, 9700, 8000, 8001, 8002, 3400, 7000, 7001, 7002, 18 | 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 6501, 7013, 19 | 7014, 7015, 7016, 6001, 6002, 6003, 6004, 6005, 6006, 6007, 5001, 5002, 20 | 5003, 5004, 5005, 9621, 4000, 4001, 8100, 8101, 8102, 8103, 3500, 3501, 21 | 3502, 3503, 3504, 3000, 7100, 9661, 7101, 7102, 7103, 7104, 7105, 7106, 22 | 7107, 7108, 7109, 7110, 7111, 7112, 7113, 7115, 7116, 7117, 7118, 7119, 23 | 7120, 6100, 6102, 6103, 6104, 6105, 6106, 6107, 6108, 6109, 6110, 6111, 24 | 6112, 6113, 6114, 6115, 6116, 6117, 6118, 6119, 6120, 6121, 6122, 6123, 25 | 6124, 5101, 5102, 5103, 5104, 6125, 5106, 5107, 5108, 5109, 5110, 5111, 26 | 5112, 5113, 5114, 5115, 5116, 5117, 5118, 5119 27 | ] 28 | }, 29 | "orgao_5": { 30 | "id_tribunal": [ 31 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 32 | 22, 23, 24 33 | ], 34 | "id_foro": [ 35 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 36 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 37 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 38 | 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 39 | 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 40 | 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 41 | 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 42 | 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 43 | 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 44 | 154, 155, 156, 157, 158, 161, 171, 172, 181, 182, 191, 192, 193, 194, 195, 45 | 201, 202, 203, 204, 205, 206, 207, 211, 221, 222, 223, 224, 225, 226, 231, 46 | 232, 233, 241, 242, 243, 244, 245, 246, 247, 251, 252, 253, 254, 255, 261, 47 | 262, 263, 264, 271, 281, 282, 291, 292, 301, 302, 303, 304, 305, 311, 312, 48 | 313, 314, 315, 316, 317, 318, 319, 321, 322, 325, 331, 332, 333, 341, 342, 49 | 343, 351, 352, 361, 371, 372, 373, 381, 382, 383, 384, 391, 401, 402, 403, 50 | 404, 411, 412, 413, 416, 421, 425, 426, 431, 432, 433, 434, 436, 441, 442, 51 | 443, 444, 445, 446, 447, 451, 459, 461, 462, 463, 464, 465, 466, 471, 472, 52 | 481, 482, 491, 492, 493, 501, 511, 512, 513, 521, 522, 531, 541, 551, 561, 53 | 562, 567, 571, 581, 585, 594, 601, 611, 612, 621, 631, 641, 643, 651, 652, 54 | 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 668, 55 | 669, 670, 671, 672, 673, 678, 701, 702, 721, 731, 732, 733, 741, 749, 751, 56 | 761, 771, 781, 791, 801, 802, 9000, 811, 812, 821, 831, 841, 851, 861, 57 | 871, 872, 892, 895, 896, 897, 898, 899, 901, 902, 903, 904, 905, 965, 999 58 | ] 59 | }, 60 | "orgao_6": { 61 | "id_tribunal": [ 62 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 63 | 22, 23, 24, 25, 26, 27 64 | ], 65 | "id_foro": [ 66 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 67 | 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 68 | 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 69 | 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 70 | 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 71 | 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 72 | 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 73 | 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 74 | 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 75 | 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 76 | 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 77 | 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 78 | 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 79 | 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 80 | 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 81 | 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 82 | 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 83 | 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 84 | 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 85 | 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 86 | 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 87 | 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 88 | 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 89 | 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 90 | 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 91 | 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 92 | 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422 93 | ] 94 | }, 95 | "orgao_7": { 96 | "id_tribunal": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 97 | "id_foro": [ 98 | 4, 101, 102, 103, 5, 201, 202, 203, 6, 301, 7, 303, 8, 401, 9, 10, 11, 12 99 | ] 100 | }, 101 | "orgao_8": { 102 | "id_tribunal": [ 103 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 104 | 22, 23, 24, 25, 26, 27 105 | ], 106 | "id_foro": [ 107 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 108 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 109 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 110 | 57, 58, 59, 60, 61, 6200, 62, 63, 64, 65, 66, 67, 68, 70, 69, 71, 72, 73, 111 | 74, 75, 76, 77, 78, 79, 81, 80, 82, 83, 84, 85, 86, 87, 88, 89, 90, 92, 112 | 91, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 4200, 103, 104, 105, 106, 113 | 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 114 | 122, 123, 124, 125, 126, 127, 128, 129, 130, 132, 133, 134, 136, 137, 138, 115 | 131, 135, 141, 142, 139, 144, 145, 146, 140, 148, 4100, 149, 150, 2200, 116 | 151, 152, 153, 6300, 154, 155, 156, 157, 158, 161, 162, 163, 164, 165, 117 | 166, 167, 168, 169, 170, 172, 173, 174, 175, 176, 177, 178, 171, 180, 181, 118 | 182, 179, 183, 184, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 119 | 197, 198, 199, 200, 201, 202, 203, 4300, 204, 205, 206, 208, 209, 210, 120 | 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 121 | 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 122 | 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 2300, 252, 253, 123 | 254, 6400, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 124 | 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 280, 281, 283, 125 | 284, 279, 282, 287, 286, 288, 290, 291, 292, 294, 295, 296, 297, 299, 300, 126 | 301, 302, 303, 4400, 306, 309, 310, 311, 312, 313, 315, 317, 318, 319, 127 | 320, 321, 322, 323, 324, 325, 2100, 327, 326, 329, 330, 331, 332, 333, 128 | 334, 335, 337, 338, 340, 341, 342, 343, 344, 346, 347, 348, 349, 350, 351, 129 | 2400, 352, 355, 6500, 356, 358, 357, 360, 361, 362, 363, 366, 368, 369, 130 | 370, 371, 372, 374, 377, 378, 380, 381, 382, 383, 384, 386, 388, 390, 391, 131 | 392, 393, 394, 395, 396, 397, 398, 400, 401, 4500, 405, 404, 407, 408, 132 | 410, 411, 412, 414, 415, 416, 417, 418, 420, 421, 422, 424, 426, 427, 428, 133 | 429, 430, 431, 432, 433, 434, 435, 438, 439, 440, 441, 443, 444, 445, 446, 134 | 447, 449, 450, 451, 2500, 452, 453, 6600, 456, 457, 459, 460, 461, 458, 135 | 462, 464, 466, 467, 470, 471, 472, 473, 474, 476, 477, 479, 480, 481, 482, 136 | 483, 484, 486, 487, 488, 490, 491, 493, 495, 498, 499, 500, 501, 4600, 137 | 505, 506, 508, 510, 511, 512, 514, 515, 516, 517, 518, 520, 521, 522, 523, 138 | 525, 526, 527, 528, 529, 530, 531, 533, 534, 538, 539, 540, 541, 542, 543, 139 | 547, 549, 550, 551, 2600, 553, 554, 555, 6700, 556, 557, 558, 559, 560, 140 | 561, 562, 563, 564, 565, 567, 568, 566, 570, 571, 572, 575, 576, 577, 579, 141 | 580, 581, 582, 584, 586, 587, 588, 589, 590, 592, 595, 596, 597, 598, 600, 142 | 601, 602, 603, 4700, 604, 606, 607, 609, 610, 611, 614, 615, 619, 620, 143 | 621, 624, 625, 627, 628, 629, 630, 631, 634, 637, 638, 640, 642, 643, 646, 144 | 647, 648, 650, 651, 2700, 653, 654, 655, 6800, 657, 659, 660, 663, 664, 145 | 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 146 | 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 147 | 695, 696, 697, 698, 699, 700, 701, 702, 703, 4800, 704, 705, 707, 708, 148 | 706, 710, 709, 713, 718, 143, 720, 730, 731, 147, 741, 740, 750, 751, 149 | 2800, 6900, 760, 761, 770, 775, 778, 780, 781, 790, 159, 800, 4900, 160, 150 | 9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 810, 9010, 151 | 820, 830, 831, 840, 850, 2900, 7000, 860, 870, 878, 879, 880, 881, 890, 152 | 900, 902, 903, 5000, 904, 905, 906, 907, 908, 910, 911, 909, 920, 930, 153 | 185, 940, 941, 950, 951, 3000, 7100, 960, 970, 980, 981, 990, 1000, 5100, 154 | 1010, 1020, 1030, 1040, 207, 1050, 3100, 7200, 1060, 1070, 1071, 1080, 155 | 1090, 1100, 5200, 1110, 1111, 1120, 1130, 1140, 1150, 3200, 7300, 1160, 156 | 1161, 1170, 1171, 1180, 1190, 1200, 1201, 5300, 1210, 1211, 1220, 1230, 157 | 1240, 1250, 3300, 7400, 1260, 1270, 1280, 1290, 1300, 5400, 1310, 1320, 158 | 1330, 1340, 1350, 3400, 7500, 1360, 1370, 1380, 1390, 1400, 5500, 1410, 159 | 1420, 1430, 1440, 1450, 3500, 7600, 1460, 1470, 1480, 1490, 1500, 5600, 160 | 1510, 1520, 1530, 1540, 1550, 3600, 1560, 1570, 1580, 1590, 1600, 5700, 161 | 1610, 1620, 1630, 3700, 7800, 5800, 3800, 5900, 9999, 3900, 6000, 4000, 162 | 2000, 2001, 2002, 2003, 6100, 2004, 2005 163 | ] 164 | }, 165 | "orgao_9": { 166 | "id_tribunal": [21, 26, 13], 167 | "id_foro": [0, 1, 2, 3, 4, 40, 10, 20, 21, 90, 91, 30] 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /brutils/date.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Union 3 | 4 | from num2words import num2words 5 | 6 | from brutils.data.enums.months import MonthsEnum 7 | 8 | 9 | def convert_date_to_text(date: str) -> Union[str, None]: 10 | """ 11 | Converts a given date in Brazilian format (dd/mm/yyyy) to its textual representation. 12 | 13 | This function takes a date as a string in the format dd/mm/yyyy and converts it 14 | to a string with the date written out in Brazilian Portuguese, including the full 15 | month name and the year. 16 | 17 | Args: 18 | date (str): The date to be converted into text. Expected format: dd/mm/yyyy. 19 | 20 | Returns: 21 | str or None: A string with the date written out in Brazilian Portuguese, 22 | or None if the date is invalid. 23 | 24 | """ 25 | pattern = re.compile(r"\d{2}/\d{2}/\d{4}") 26 | if not re.match(pattern, date): 27 | raise ValueError( 28 | "Date is not a valid date. Please pass a date in the format dd/mm/yyyy." 29 | ) 30 | 31 | day_str, month_str, year_str = date.split("/") 32 | day = int(day_str) 33 | month = int(month_str) 34 | year = int(year_str) 35 | 36 | if 0 <= day > 31: 37 | return None 38 | 39 | if not MonthsEnum.is_valid_month(month): 40 | return None 41 | 42 | # Leap year. 43 | if MonthsEnum(int(month)) is MonthsEnum.FEVEREIRO: 44 | if (int(year) % 4 == 0 and int(year) % 100 != 0) or ( 45 | int(year) % 400 == 0 46 | ): 47 | if day > 29: 48 | return None 49 | else: 50 | if day > 28: 51 | return None 52 | 53 | day_string = "Primeiro" if day == 1 else num2words(day, lang="pt") 54 | month = MonthsEnum(month) 55 | year_string = num2words(year, lang="pt") 56 | 57 | date_string = ( 58 | day_string.capitalize() 59 | + " de " 60 | + month.month_name 61 | + " de " 62 | + year_string 63 | ) 64 | return date_string 65 | -------------------------------------------------------------------------------- /brutils/date_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Union 3 | 4 | import holidays 5 | 6 | 7 | def is_holiday(target_date: datetime, uf: str = None) -> Union[bool, None]: 8 | """ 9 | Checks if the given date is a national or state holiday in Brazil. 10 | 11 | This function takes a date as a `datetime` object and an optional UF (Unidade Federativa), 12 | returning a boolean value indicating whether the date is a holiday or `None` if the date or 13 | UF are invalid. 14 | 15 | The method does not handle municipal holidays. 16 | 17 | Args: 18 | target_date (datetime): The date to be checked. 19 | uf (str, optional): The state abbreviation (UF) to check for state holidays. 20 | If not provided, only national holidays will be considered. 21 | 22 | Returns: 23 | bool | None: Returns `True` if the date is a holiday, `False` if it is not, 24 | or `None` if the date or UF are invalid. 25 | 26 | Note: 27 | The function logic should be implemented using the `holidays` library. 28 | For more information, refer to the documentation at: https://pypi.org/project/holidays/ 29 | 30 | Usage Examples: 31 | >>> from datetime import datetime 32 | >>> is_holiday(datetime(2024, 1, 1)) 33 | True 34 | 35 | >>> is_holiday(datetime(2024, 1, 2)) 36 | False 37 | 38 | >>> is_holiday(datetime(2024, 3, 2), uf="SP") 39 | False 40 | 41 | >>> is_holiday(datetime(2024, 12, 25), uf="RJ") 42 | True 43 | """ 44 | 45 | if not isinstance(target_date, datetime): 46 | return None 47 | 48 | valid_ufs = holidays.Brazil().subdivisions 49 | if uf is not None and uf not in valid_ufs: 50 | return None 51 | 52 | national_holidays = holidays.Brazil(years=target_date.year) 53 | 54 | if uf is None: 55 | return target_date in national_holidays 56 | 57 | state_holidays = holidays.Brazil(prov=uf, years=target_date.year) 58 | return target_date in state_holidays 59 | -------------------------------------------------------------------------------- /brutils/email.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def is_valid(email): # type: (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 | -------------------------------------------------------------------------------- /brutils/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .cep import CEPNotFound, InvalidCEP 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /brutils/ibge/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brazilian-utils/brutils-python/c6cb92acfcfc81fe16e2d9aea8a58eb9689dce07/brutils/ibge/__init__.py -------------------------------------------------------------------------------- /brutils/ibge/municipality.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import io 3 | import json 4 | import pathlib 5 | import unicodedata 6 | from urllib.error import HTTPError 7 | from urllib.request import urlopen 8 | 9 | 10 | def get_municipality_by_code(code): # type: (str) -> Tuple[str, str] | None 11 | """ 12 | Returns the municipality name and UF for a given IBGE code. 13 | 14 | This function takes a string representing an IBGE municipality code 15 | and returns a tuple with the municipality's name and its corresponding UF. 16 | 17 | Args: 18 | code (str): The IBGE code of the municipality. 19 | 20 | Returns: 21 | tuple: A tuple formatted as ("Município", "UF"). 22 | - Returns None if the code is not valid. 23 | 24 | Example: 25 | >>> get_municipality_by_code("3550308") 26 | ("São Paulo", "SP") 27 | """ 28 | baseUrl = ( 29 | f"https://servicodados.ibge.gov.br/api/v1/localidades/municipios/{code}" 30 | ) 31 | try: 32 | with urlopen(baseUrl) as f: 33 | compressed_data = f.read() 34 | if f.info().get("Content-Encoding") == "gzip": 35 | try: 36 | with gzip.GzipFile( 37 | fileobj=io.BytesIO(compressed_data) 38 | ) as gzip_file: 39 | decompressed_data = gzip_file.read() 40 | except OSError as e: 41 | print(f"Erro ao descomprimir os dados: {e}") 42 | return None 43 | except Exception as e: 44 | print(f"Erro desconhecido ao descomprimir os dados: {e}") 45 | return None 46 | else: 47 | decompressed_data = compressed_data 48 | 49 | if _is_empty(decompressed_data): 50 | print(f"{code} é um código inválido") 51 | return None 52 | 53 | except HTTPError as e: 54 | if e.code == 404: 55 | print(f"{code} é um código inválido") 56 | return None 57 | else: 58 | print(f"Erro HTTP ao buscar o código {code}: {e}") 59 | return None 60 | 61 | except Exception as e: 62 | print(f"Erro desconhecido ao buscar o código {code}: {e}") 63 | return None 64 | 65 | try: 66 | json_data = json.loads(decompressed_data) 67 | return _get_values(json_data) 68 | except json.JSONDecodeError as e: 69 | print(f"Erro ao decodificar os dados JSON: {e}") 70 | return None 71 | except KeyError as e: 72 | print(f"Erro ao acessar os dados do município: {e}") 73 | return None 74 | 75 | 76 | def get_code_by_municipality_name(municipality_name: str, uf: str): # type: (str, str) -> str | None 77 | """ 78 | Returns the IBGE code for a given municipality name and uf code. 79 | 80 | This function takes a string representing a municipality's name 81 | and uf's code and returns the corresponding IBGE code (string). The function 82 | will handle names by ignoring differences in case, accents, and 83 | treating the character ç as c and ignoring case differences for the uf code. 84 | 85 | Args: 86 | municipality_name (str): The name of the municipality. 87 | uf (str): The uf code of the state. 88 | 89 | Returns: 90 | str: The IBGE code of the municipality. 91 | - Returns None if the name is not valid or does not exist. 92 | 93 | Example: 94 | >>> get_code_by_municipality_name("São Paulo", "SP") 95 | "3550308" 96 | >>> get_code_by_municipality_name("goiania", "go") 97 | "5208707" 98 | >>> get_code_by_municipality_name("Conceição do Coité", "BA") 99 | "2908408" 100 | >>> get_code_by_municipality_name("conceicao do Coite", "Ba") 101 | "2908408" 102 | >>> get_code_by_municipality_name("Municipio Inexistente", "") 103 | None 104 | >>> get_code_by_municipality_name("Municipio Inexistente", "RS") 105 | None 106 | """ 107 | 108 | abs_path = pathlib.Path(__file__).resolve() 109 | script_dir = abs_path.parent.parent 110 | 111 | json_cities_code_path = script_dir / "data" / "cities_code.json" 112 | uf = uf.upper() 113 | 114 | with open(json_cities_code_path, "r", encoding="utf-8") as file: 115 | cities_uf_code = json.load(file) 116 | 117 | if uf not in cities_uf_code.keys(): 118 | return None 119 | 120 | cities_code = cities_uf_code.get(uf) 121 | name_city = _transform_text(municipality_name) 122 | 123 | if name_city not in cities_code.keys(): 124 | return None 125 | 126 | code = cities_code.get(name_city) 127 | 128 | return code 129 | 130 | 131 | def _get_values(data): 132 | municipio = data["nome"] 133 | estado = data["microrregiao"]["mesorregiao"]["UF"]["sigla"] 134 | return (municipio, estado) 135 | 136 | 137 | def _is_empty(zip): 138 | return zip == b"[]" or len(zip) == 0 139 | 140 | 141 | def _transform_text(municipality_name: str): # type: (str) -> str 142 | """ 143 | Normalize municipality name and returns the normalized string. 144 | 145 | Args: 146 | municipality_name (str): The name of the municipality. 147 | 148 | Returns: 149 | str: The normalized string 150 | 151 | Example: 152 | >>> _transform_text("São Paulo") 153 | "sao paulo" 154 | >>> _transform_text("Goiânia") 155 | "goiania" 156 | >>> _transform_text("Conceição do Coité") 157 | "'conceicao do coite' 158 | """ 159 | 160 | normalized_string = ( 161 | unicodedata.normalize("NFKD", municipality_name) 162 | .encode("ascii", "ignore") 163 | .decode("ascii") 164 | ) 165 | case_fold_string = normalized_string.casefold() 166 | 167 | return case_fold_string 168 | -------------------------------------------------------------------------------- /brutils/ibge/uf.py: -------------------------------------------------------------------------------- 1 | from brutils.data.enums.uf import CODE_TO_UF 2 | 3 | 4 | def convert_code_to_uf(code): # type: (str) -> str | None 5 | """ 6 | Converts a given IBGE code (2-digit string) to its corresponding UF (state abbreviation). 7 | 8 | This function takes a 2-digit IBGE code and returns the corresponding UF code. 9 | It handles all Brazilian states and the Federal District. 10 | 11 | Args: 12 | code (str): The 2-digit IBGE code to be converted. 13 | 14 | Returns: 15 | str or None: The UF code corresponding to the IBGE code, 16 | or None if the IBGE code is invalid. 17 | 18 | Example: 19 | >>> convert_code_to_uf('12') 20 | 'AC' 21 | >>> convert_code_to_uf('33') 22 | 'RJ' 23 | >>> convert_code_to_uf('99') 24 | >>> 25 | """ 26 | 27 | result = None 28 | 29 | if code in CODE_TO_UF.values: 30 | result = CODE_TO_UF(code).name 31 | 32 | return result 33 | -------------------------------------------------------------------------------- /brutils/legal_process.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | from datetime import datetime 5 | from random import randint 6 | 7 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 8 | DATA_DIR = f"{ROOT_DIR}/data" 9 | VALID_IDS_FILE = f"{DATA_DIR}/legal_process_ids.json" 10 | 11 | # FORMATTING 12 | ############ 13 | 14 | 15 | def remove_symbols(legal_process: str): # type: (str) -> str 16 | """ 17 | Removes specific symbols from a given legal process. 18 | 19 | This function takes a legal process as input and removes all occurrences 20 | of the '.' and '-' characters from it. 21 | 22 | Args: 23 | legal_process (str): A legal process containing symbols to be removed. 24 | 25 | Returns: 26 | str: The legal process string with the specified symbols removed. 27 | 28 | Example: 29 | >>> remove_symbols("123.45-678.901.234-56.7890") 30 | '12345678901234567890' 31 | >>> remove_symbols("9876543-21.0987.6.54.3210") 32 | '98765432109876543210' 33 | """ 34 | 35 | return legal_process.replace(".", "").replace("-", "") 36 | 37 | 38 | def format_legal_process(legal_process_id): # type: (str) -> (str) 39 | """ 40 | Format a legal process ID into a standard format. 41 | 42 | Args: 43 | legal_process_id (str): A 20-digits string representing the legal 44 | process ID. 45 | 46 | Returns: 47 | str: The formatted legal process ID or None if the input is invalid. 48 | 49 | Example: 50 | >>> format_legal_process("12345678901234567890") 51 | '1234567-89.0123.4.56.7890' 52 | >>> format_legal_process("98765432109876543210") 53 | '9876543-21.0987.6.54.3210' 54 | >>> format_legal_process("123") 55 | None 56 | """ 57 | 58 | if legal_process_id.isdigit() and len(legal_process_id) == 20: 59 | capture_fields = r"(\d{7})(\d{2})(\d{4})(\d)(\d{2})(\d{4})" 60 | include_chars = r"\1-\2.\3.\4.\5.\6" 61 | 62 | return re.sub(capture_fields, include_chars, legal_process_id) 63 | 64 | return None 65 | 66 | 67 | # OPERATIONS 68 | ############ 69 | 70 | 71 | def is_valid(legal_process_id): # type: (str) -> bool 72 | """ 73 | Check if a legal process ID is valid. 74 | 75 | This function does not verify if the legal process ID is a real legal 76 | process ID; it only validates the format of the string. 77 | 78 | Args: 79 | legal_process_id (str): A digit-only string representing the legal 80 | process ID. 81 | 82 | Returns: 83 | bool: True if the legal process ID is valid, False otherwise. 84 | 85 | Example: 86 | >>> is_valid("68476506020233030000") 87 | True 88 | >>> is_valid("51808233620233030000") 89 | True 90 | >>> is_valid("123") 91 | False 92 | """ 93 | 94 | clean_legal_process_id = remove_symbols(legal_process_id) 95 | DD = clean_legal_process_id[7:9] 96 | J = clean_legal_process_id[13:14] 97 | TR = clean_legal_process_id[14:16] 98 | OOOO = clean_legal_process_id[16:] 99 | 100 | with open(VALID_IDS_FILE) as file: 101 | legal_process_ids = json.load(file) 102 | process = legal_process_ids.get(f"orgao_{J}") 103 | if not process: 104 | return False 105 | valid_process = int(TR) in process.get("id_tribunal") and int( 106 | OOOO 107 | ) in process.get("id_foro") 108 | 109 | return ( 110 | _checksum(int(clean_legal_process_id[0:7] + clean_legal_process_id[9:])) 111 | == DD 112 | ) and valid_process 113 | 114 | 115 | def generate(year=datetime.now().year, orgao=randint(1, 9)): # type: (int, int) -> (str) 116 | """ 117 | Generate a random legal process ID number. 118 | 119 | Args: 120 | year (int): The year for the legal process ID (default is the current 121 | year). 122 | The year should not be in the past 123 | orgao (int): The organization code (1-9) for the legal process ID 124 | (default is random). 125 | 126 | Returns: 127 | str: A randomly generated legal process ID. 128 | None if one of the arguments is invalid. 129 | 130 | Example: 131 | >>> generate(2023, 5) 132 | '51659517020235080562' 133 | >>> generate() 134 | '88031888120233030000' 135 | >>> generate(2022, 10) 136 | None 137 | """ 138 | 139 | if year < datetime.now().year or orgao not in range(1, 10): 140 | return None 141 | 142 | # Getting possible legal process ids from 'legal_process_ids.json' asset 143 | with open(VALID_IDS_FILE) as file: 144 | legal_process_ids = json.load(file) 145 | _ = legal_process_ids[f"orgao_{orgao}"] 146 | TR = str( 147 | _["id_tribunal"][randint(0, (len(_["id_tribunal"]) - 1))] 148 | ).zfill(2) 149 | OOOO = str(_["id_foro"][randint(0, (len(_["id_foro"])) - 1)]).zfill(4) 150 | NNNNNNN = str(randint(0, 9999999)).zfill(7) 151 | DD = _checksum(f"{NNNNNNN}{year}{orgao}{TR}{OOOO}") 152 | 153 | return f"{NNNNNNN}{DD}{year}{orgao}{TR}{OOOO}" 154 | 155 | 156 | def _checksum(basenum): # type: (int) -> str 157 | """ 158 | Checksum to compute the verification digit for a Legal Process ID number. 159 | `basenum` needs to be a digit without the verification id. 160 | 161 | Args: 162 | basenum (int): The base number for checksum calculation. 163 | 164 | Returns: 165 | str: The checksum value as a string. 166 | 167 | Example: 168 | >>> _checksum(1234567) 169 | '50' 170 | >>> _checksum(9876543) 171 | '88' 172 | """ 173 | 174 | return str(97 - ((int(basenum) * 100) % 97)).zfill(2) 175 | -------------------------------------------------------------------------------- /brutils/license_plate.py: -------------------------------------------------------------------------------- 1 | import re 2 | from random import choice, randint 3 | from string import ascii_uppercase 4 | from typing import Optional 5 | 6 | # FORMATTING 7 | ############ 8 | 9 | 10 | def convert_to_mercosul(license_plate: str) -> Optional[str]: 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 | Optional[str]: 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) -> Optional[str]: 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 | Optional[str]: 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(license_plate, type=None): # type: (str, str) -> bool 73 | """ 74 | Returns if a Brazilian license plate number is valid. 75 | It does not verify if the plate actually exists. 76 | 77 | Args: 78 | license_plate (str): The license plate number to be validated. 79 | type (str): "old_format" or "mercosul". 80 | If not specified, checks for one or another. 81 | Returns: 82 | bool: True if the plate number is valid. False otherwise. 83 | """ 84 | 85 | if type == "old_format": 86 | return _is_valid_old_format(license_plate) 87 | if type == "mercosul": 88 | return _is_valid_mercosul(license_plate) 89 | 90 | return _is_valid_old_format(license_plate) or _is_valid_mercosul( 91 | license_plate 92 | ) 93 | 94 | 95 | def remove_symbols(license_plate_number: str) -> str: 96 | """ 97 | Removes the dash (-) symbol from a license plate string. 98 | 99 | Args: 100 | license_plate_number (str): A license plate number containing symbols to 101 | be removed. 102 | 103 | Returns: 104 | str: The license plate number with the specified symbols removed. 105 | 106 | Example: 107 | >>> remove_symbols("ABC-123") 108 | "ABC123" 109 | >>> remove_symbols("abc123") 110 | "abc123" 111 | >>> remove_symbols("ABCD123") 112 | "ABCD123" 113 | """ 114 | 115 | return license_plate_number.replace("-", "") 116 | 117 | 118 | def get_format(license_plate: str) -> Optional[str]: 119 | """ 120 | Return the format of a license plate. 'LLLNNNN' for the old pattern and 121 | 'LLLNLNN' for the Mercosul one. 122 | 123 | Args: 124 | license_plate (str): A license plate string without symbols. 125 | 126 | Returns: 127 | str: The format of the license plate (LLLNNNN, LLLNLNN) or 128 | 'None' if the format is invalid. 129 | 130 | Example: 131 | >>> get_format("abc123") 132 | "LLLNNNN" 133 | >>> get_format("abc1d23") 134 | "LLLNLNN" 135 | >>> get_format("ABCD123") 136 | None 137 | """ 138 | 139 | if _is_valid_old_format(license_plate): 140 | return "LLLNNNN" 141 | 142 | if _is_valid_mercosul(license_plate): 143 | return "LLLNLNN" 144 | 145 | return None 146 | 147 | 148 | def generate(format="LLLNLNN"): # type: (str) -> str | None 149 | """ 150 | Generate a valid license plate in the given format. In case no format is 151 | provided, it will return a license plate in the Mercosul format. 152 | 153 | Args: 154 | format (str): The desired format for the license plate. 155 | 'LLLNNNN' for the old pattern or 'LLLNLNN' for the 156 | Mercosul one. Default is 'LLLNLNN' 157 | 158 | Returns: 159 | str: A randomly generated license plate number or 160 | 'None' if the format is invalid. 161 | 162 | Example: 163 | >>> generate() 164 | "ABC1D23" 165 | >>> generate(format="LLLNLNN") 166 | "ABC4D56" 167 | >>> generate(format="LLLNNNN") 168 | "ABC123" 169 | >>> generate(format="invalid") 170 | None 171 | """ 172 | 173 | generated = "" 174 | 175 | format = format.upper() 176 | 177 | if format not in ("LLLNLNN", "LLLNNNN"): 178 | return None 179 | 180 | for char in format: 181 | if char == "L": 182 | generated += choice(ascii_uppercase) 183 | else: 184 | generated += str(randint(0, 9)) 185 | 186 | return generated 187 | 188 | 189 | def _is_valid_old_format(license_plate: str) -> bool: 190 | """ 191 | Checks whether a string matches the old format of Brazilian license plate. 192 | Args: 193 | license_plate (str): The desired format for the license plate. 194 | pattern:'LLLNNNN' 195 | 196 | Returns: 197 | bool: True if the plate number is valid. False otherwise. 198 | 199 | """ 200 | pattern = re.compile(r"^[A-Za-z]{3}[0-9]{4}$") 201 | return ( 202 | isinstance(license_plate, str) 203 | and re.match(pattern, license_plate.strip()) is not None 204 | ) 205 | 206 | 207 | def _is_valid_mercosul(license_plate: str) -> bool: 208 | """ 209 | Checks whether a string matches the old format of Brazilian license plate. 210 | Args: 211 | license_plate (str): The desired format for the license plate. 212 | pattern:'LLLNLNN' 213 | 214 | Returns: 215 | bool: True if the plate number is valid. False otherwise. 216 | 217 | """ 218 | if not isinstance(license_plate, str): 219 | return False 220 | 221 | license_plate = license_plate.upper().strip() 222 | pattern = re.compile(r"^[A-Z]{3}\d[A-Z]\d{2}$") 223 | return re.match(pattern, license_plate) is not None 224 | -------------------------------------------------------------------------------- /brutils/phone.py: -------------------------------------------------------------------------------- 1 | import re 2 | from random import choice, randint 3 | 4 | 5 | # FORMATTING 6 | ############ 7 | def format_phone(phone): # type: (str) -> str 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, type=None): # type: (str, str) -> 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): # type: (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=None): # type: (str) -> 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): # type: (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): # type: (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): # type: (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(): # type() -> 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(): 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(): # type () -> 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 | -------------------------------------------------------------------------------- /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/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .address import Address 2 | -------------------------------------------------------------------------------- /brutils/types/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 | -------------------------------------------------------------------------------- /brutils/voter_id.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | 4 | def is_valid(voter_id): # type: (str) -> bool 5 | """ 6 | Check if a Brazilian voter id number is valid. 7 | It does not verify if the voter id actually exists. 8 | 9 | References: 10 | - https://pt.wikipedia.org/wiki/T%C3%ADtulo_de_eleitor, 11 | - http://clubes.obmep.org.br/blog/a-matematica-nos-documentos-titulo-de-eleitor/ 12 | 13 | Args: 14 | voter_id(str): string representing the voter id to be verified. 15 | 16 | Returns: 17 | bool: True if the voter id is valid. False otherwise. 18 | """ 19 | 20 | # ensure voter_id only contains numerical characters and has a valid length 21 | if ( 22 | not isinstance(voter_id, str) 23 | or not voter_id.isdigit() 24 | or not _is_length_valid(voter_id) 25 | ): 26 | return False 27 | 28 | # the voter id is composed of 3 parts, in this order: 29 | # - sequential_number (8 or 9 digits) 30 | # - federative_union (2 digits) 31 | # - verifying digits (2 digits) 32 | sequential_number = _get_sequential_number(voter_id) 33 | federative_union = _get_federative_union(voter_id) 34 | verifying_digits = _get_verifying_digits(voter_id) 35 | 36 | # ensure federative union is valid 37 | if not _is_federative_union_valid(federative_union): 38 | return False 39 | 40 | # ensure the first verifying digit is valid 41 | vd1 = _calculate_vd1(sequential_number, federative_union) 42 | if vd1 != int(verifying_digits[0]): 43 | return False 44 | 45 | # ensure the second verifying digit is valid 46 | vd2 = _calculate_vd2(federative_union, vd1) 47 | if vd2 != int(verifying_digits[1]): 48 | return False 49 | 50 | return True 51 | 52 | 53 | def _is_length_valid(voter_id): # type: (str) -> bool 54 | """ 55 | Check if the length of the provided voter id is valid. 56 | Typically, the length should be 12, but there are cases for SP and MG where 57 | the length might be 13. This occurs as an edge case when the sequential 58 | number has 9 digits instead of 8. 59 | 60 | Args: 61 | voter_id (str): string representing the voter id to be verified 62 | 63 | Returns: 64 | bool: True if the length is valid. False otherwise. 65 | """ 66 | 67 | federative_union = _get_federative_union(voter_id) 68 | 69 | if len(voter_id) == 12: 70 | return True 71 | 72 | # edge case: for SP and MG with 9 digit long 'sequential number' 73 | return len(voter_id) == 13 and federative_union in ["01", "02"] 74 | 75 | 76 | def _get_sequential_number(voter_id): # type: (str) -> str 77 | """ 78 | Some voter ids in São Paulo and Minas Gerais may have nine digits in their 79 | sequential number instead of eight. This fact does not compromise the 80 | calculation of the check digits, as it is done based on the first eight 81 | sequential numbers. 82 | 83 | Args: 84 | voter_id (str): string representing the voter id to be verified. 85 | 86 | Returns: 87 | str: eight-digit string representing the sequential number for the 88 | given voter id 89 | """ 90 | 91 | return voter_id[:8] 92 | 93 | 94 | def _get_federative_union(voter_id): # type: (str) -> str 95 | """ 96 | Returns the two digits that represent the federative union for the given 97 | voter id. Indexing it backwards, as the sequential_number can have eight 98 | or nine digits. 99 | 100 | Args: 101 | voter_id (str): string representing the voter id to be verified 102 | 103 | Returns: 104 | str: two-digits string representing the federative union for the given voter id 105 | """ 106 | 107 | return voter_id[-4:-2] 108 | 109 | 110 | def _get_verifying_digits(voter_id): # type: (str) -> str 111 | """ 112 | Returns the two verifying digits for the given voter id. Indexing it 113 | backwards, as the sequential_number can have eight or nine digits. 114 | 115 | Args: 116 | voter_id (str): string representing the voter id to be verified 117 | 118 | Returns: 119 | str: the two verifying digits for the given voter id 120 | """ 121 | 122 | return voter_id[-2:] 123 | 124 | 125 | def _is_federative_union_valid(federative_union): # type: (str) -> bool 126 | """ 127 | Check if the federative union is valid. 128 | 129 | Args: 130 | federative_union(str): federative union for the given voter id 131 | 132 | Returns: 133 | bool: True if the federative union is valid. False otherwise. 134 | """ 135 | 136 | # valid federative unions: from '01' to '28' 137 | return federative_union in ["{:02d}".format(i) for i in range(1, 29)] 138 | 139 | 140 | def _calculate_vd1(sequential_number, federative_union): # type: (str, str) -> bool 141 | """ 142 | Calculate the first verifying digit. 143 | 144 | Args: 145 | sequential_number(str): sequential number sliced from the voter_id 146 | federative_union(str): federative union for the given voter id 147 | 148 | Returns: 149 | int: the expected value for the first verifying digit for the given 150 | voter id 151 | """ 152 | 153 | # 2, 3, 4, 5, 6, 7, 8, 9 154 | x1, x2, x3, x4, x5, x6, x7, x8 = range(2, 10) 155 | 156 | sum = ( 157 | (int(sequential_number[0]) * x1) 158 | + (int(sequential_number[1]) * x2) 159 | + (int(sequential_number[2]) * x3) 160 | + (int(sequential_number[3]) * x4) 161 | + (int(sequential_number[4]) * x5) 162 | + (int(sequential_number[5]) * x6) 163 | + (int(sequential_number[6]) * x7) 164 | + (int(sequential_number[7]) * x8) 165 | ) 166 | 167 | rest = sum % 11 168 | vd1 = rest 169 | 170 | # edge case: rest == 0 and federative_union is SP or MG 171 | if rest == 0 and federative_union in ["01", "02"]: 172 | vd1 = 1 173 | 174 | # edge case: rest == 10, declare vd1 as zero 175 | if rest == 10: 176 | vd1 = 0 177 | 178 | return vd1 179 | 180 | 181 | def _calculate_vd2(federative_union, vd1): # type: (str, int) -> str 182 | """ 183 | Calculate the second verifying digit. 184 | 185 | Args: 186 | federative_union(str): federative union for the given voter id 187 | vd1(int): first verifying digit calculated for the given voter id 188 | 189 | Returns: 190 | int: the expected value for the second verifying digit for the given 191 | voter id 192 | """ 193 | 194 | x9, x10, x11 = 7, 8, 9 195 | 196 | sum = ( 197 | (int(federative_union[0]) * x9) 198 | + (int(federative_union[1]) * x10) 199 | + (vd1 * x11) 200 | ) 201 | 202 | rest = sum % 11 203 | vd2 = rest 204 | 205 | # edge case: if federative_union is "01" or "02" (for SP and MG) AND 206 | # rest == 0, declare vd2 as 1 207 | if federative_union in ["01", "02"] and rest == 0: 208 | vd2 = 1 209 | 210 | # edge case: rest == 10, declare vd2 as zero 211 | if rest == 10: 212 | vd2 = 0 213 | 214 | return vd2 215 | 216 | 217 | def generate(federative_union="ZZ") -> str: 218 | """ 219 | Generates a random valid Brazilian voter registration. 220 | 221 | Args: 222 | federative_union(str): federative union for the voter id that will be generated. The default value "ZZ" is used for voter IDs issued to foreigners. 223 | 224 | Returns: 225 | str: A randomly generated valid voter ID for the given federative union 226 | """ 227 | UFs = { 228 | "SP": "01", 229 | "MG": "02", 230 | "RJ": "03", 231 | "RS": "04", 232 | "BA": "05", 233 | "PR": "06", 234 | "CE": "07", 235 | "PE": "08", 236 | "SC": "09", 237 | "GO": "10", 238 | "MA": "11", 239 | "PB": "12", 240 | "PA": "13", 241 | "ES": "14", 242 | "PI": "15", 243 | "RN": "16", 244 | "AL": "17", 245 | "MT": "18", 246 | "MS": "19", 247 | "DF": "20", 248 | "SE": "21", 249 | "AM": "22", 250 | "RO": "23", 251 | "AC": "24", 252 | "AP": "25", 253 | "RR": "26", 254 | "TO": "27", 255 | "ZZ": "28", 256 | } 257 | 258 | federative_union = federative_union.upper() 259 | if federative_union in (UFs): 260 | sequential_number = str(randint(0, 99999999)).zfill(8) 261 | uf_number = UFs[federative_union] 262 | if _is_federative_union_valid(uf_number): 263 | vd1 = _calculate_vd1(sequential_number, uf_number) 264 | vd2 = _calculate_vd2(uf_number, vd1) 265 | return f"{sequential_number}{uf_number}{vd1}{vd2}" 266 | 267 | 268 | def format_voter_id(voter_id): # type: (str) -> str 269 | """ 270 | Format a voter ID for display with visual spaces. 271 | 272 | This function takes a numeric voter ID string as input and adds standard 273 | formatting for display purposes. 274 | 275 | Args: 276 | voter_id (str): A numeric voter ID string. 277 | 278 | Returns: 279 | str: A formatted voter ID string with standard visual spacing, or None 280 | if the input is invalid. 281 | 282 | Example: 283 | >>> format_voter_id("690847092828") 284 | '6908 4709 28 28' 285 | >>> format_voter_id("163204010922") 286 | '1632 0401 09 22' 287 | """ 288 | 289 | if not is_valid(voter_id): 290 | return None 291 | 292 | return "{} {} {} {}".format( 293 | voter_id[:4], voter_id[4:8], voter_id[8:10], voter_id[10:12] 294 | ) 295 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "tests" 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "brutils" 3 | version = "2.2.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.8", 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Topic :: Office/Business", 21 | "Topic :: Software Development :: Internationalization", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | "Natural Language :: English", 24 | "Natural Language :: Portuguese", 25 | "Natural Language :: Portuguese (Brazilian)" 26 | ] 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.8.1" 30 | holidays = "^0.58" 31 | num2words = "0.5.14" 32 | coverage = "^7.2.7" 33 | 34 | [tool.poetry.group.test.dependencies] 35 | coverage = "^7.2.7" 36 | 37 | [tool.poetry.group.dev.dependencies] 38 | ruff = ">=0.5.0,<0.7.0" 39 | 40 | [tool.ruff] 41 | line-length = 80 42 | lint.extend-select = ["I"] 43 | 44 | [tool.ruff.lint.per-file-ignores] 45 | "__init__.py" = ["F401"] 46 | 47 | [build-system] 48 | requires = ["poetry-core"] 49 | build-backend = "poetry.core.masonry.api" 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brazilian-utils/brutils-python/c6cb92acfcfc81fe16e2d9aea8a58eb9689dce07/tests/__init__.py -------------------------------------------------------------------------------- /tests/ibge/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brazilian-utils/brutils-python/c6cb92acfcfc81fe16e2d9aea8a58eb9689dce07/tests/ibge/__init__.py -------------------------------------------------------------------------------- /tests/ibge/test_municipality.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | from json import JSONDecodeError, dumps 3 | from unittest import TestCase, main 4 | from unittest.mock import MagicMock, patch 5 | from urllib.error import HTTPError 6 | 7 | from brutils.ibge.municipality import ( 8 | get_code_by_municipality_name, 9 | get_municipality_by_code, 10 | ) 11 | 12 | 13 | class TestIBGE(TestCase): 14 | @patch("brutils.ibge.municipality.urlopen") 15 | def test_get_municipality_by_code(self, mock): 16 | def mock_response(url): 17 | responses = { 18 | "https://servicodados.ibge.gov.br/api/v1/localidades/municipios/3550308": dumps( 19 | { 20 | "nome": "São Paulo", 21 | "microrregiao": { 22 | "mesorregiao": {"UF": {"sigla": "SP"}} 23 | }, 24 | } 25 | ).encode("utf-8"), 26 | "https://servicodados.ibge.gov.br/api/v1/localidades/municipios/3304557": dumps( 27 | { 28 | "nome": "Rio de Janeiro", 29 | "microrregiao": { 30 | "mesorregiao": {"UF": {"sigla": "RJ"}} 31 | }, 32 | } 33 | ).encode("utf-8"), 34 | "https://servicodados.ibge.gov.br/api/v1/localidades/municipios/5208707": dumps( 35 | { 36 | "nome": "Goiânia", 37 | "microrregiao": { 38 | "mesorregiao": {"UF": {"sigla": "GO"}} 39 | }, 40 | } 41 | ).encode("utf-8"), 42 | "https://servicodados.ibge.gov.br/api/v1/localidades/municipios/1234567": b"[]", 43 | } 44 | 45 | mock_file = MagicMock() 46 | mock_file.__enter__.return_value = ( 47 | mock_file # mock_file is a context manager 48 | ) 49 | mock_file.read.return_value = responses.get(url, b"[]") 50 | mock_file.info.return_value = {"Content-Encoding": None} 51 | return mock_file 52 | 53 | mock.side_effect = mock_response 54 | 55 | self.assertEqual( 56 | get_municipality_by_code("3550308"), ("São Paulo", "SP") 57 | ) 58 | self.assertEqual( 59 | get_municipality_by_code("3304557"), ("Rio de Janeiro", "RJ") 60 | ) 61 | self.assertEqual(get_municipality_by_code("5208707"), ("Goiânia", "GO")) 62 | self.assertIsNone(get_municipality_by_code("1234567")) 63 | 64 | @patch("brutils.ibge.municipality.urlopen") 65 | def test_get_municipality_http_error(self, mock): 66 | mock.side_effect = HTTPError( 67 | "http://fakeurl.com", 404, "Not Found", None, None 68 | ) 69 | result = get_municipality_by_code("342432") 70 | self.assertIsNone(result) 71 | 72 | @patch("brutils.ibge.municipality.urlopen") 73 | def test_get_municipality_http_error_1(self, mock): 74 | mock.side_effect = HTTPError( 75 | "http://fakeurl.com", 401, "Denied", None, None 76 | ) 77 | result = get_municipality_by_code("342432") 78 | self.assertIsNone(result) 79 | 80 | @patch("brutils.ibge.municipality.urlopen") 81 | def test_get_municipality_excpetion(self, mock): 82 | mock.side_effect = Exception("Erro desconhecido") 83 | result = get_municipality_by_code("342432") 84 | self.assertIsNone(result) 85 | 86 | @patch("brutils.ibge.municipality.urlopen") 87 | def test_successfull_decompression(self, mock_urlopen): 88 | valid_json = '{"nome":"São Paulo","microrregiao":{"mesorregiao":{"UF":{"sigla":"SP"}}}}' 89 | compressed_data = gzip.compress(valid_json.encode("utf-8")) 90 | mock_response = MagicMock() 91 | mock_response.read.return_value = compressed_data 92 | mock_response.info.return_value.get.return_value = "gzip" 93 | mock_urlopen.return_value.__enter__.return_value = mock_response 94 | 95 | result = get_municipality_by_code("3550308") 96 | self.assertEqual(result, ("São Paulo", "SP")) 97 | 98 | @patch("brutils.ibge.municipality.urlopen") 99 | def test_successful_json_without_compression(self, mock_urlopen): 100 | valid_json = '{"nome":"São Paulo","microrregiao":{"mesorregiao":{"UF":{"sigla":"SP"}}}}' 101 | mock_response = MagicMock() 102 | mock_response.read.return_value = valid_json 103 | mock_urlopen.return_value.__enter__.return_value = mock_response 104 | 105 | result = get_municipality_by_code("3550308") 106 | self.assertEqual(result, ("São Paulo", "SP")) 107 | 108 | @patch("gzip.GzipFile.read", side_effect=OSError("Erro na descompressão")) 109 | def test_error_decompression(self, mock_gzip_read): 110 | result = get_municipality_by_code("3550308") 111 | self.assertIsNone(result) 112 | 113 | @patch( 114 | "gzip.GzipFile.read", 115 | side_effect=Exception("Erro desconhecido na descompressão"), 116 | ) 117 | def test_error_decompression_generic_exception(self, mock_gzip_read): 118 | result = get_municipality_by_code("3550308") 119 | self.assertIsNone(result) 120 | 121 | @patch("json.loads", side_effect=JSONDecodeError("error", "city.json", 1)) 122 | def test_error_json_load(self, mock_json_loads): 123 | result = get_municipality_by_code("3550308") 124 | self.assertIsNone(result) 125 | 126 | @patch("json.loads", side_effect=KeyError) 127 | def test_error_json_key_error(self, mock_json_loads): 128 | result = get_municipality_by_code("3550308") 129 | self.assertIsNone(result) 130 | 131 | def test_get_code_by_municipality_name(self): 132 | self.assertEqual( 133 | get_code_by_municipality_name("Florianópolis", "sc"), "4205407" 134 | ) 135 | self.assertEqual( 136 | get_code_by_municipality_name("São Paulo", "sp"), "3550308" 137 | ) 138 | self.assertEqual( 139 | get_code_by_municipality_name("GOIANIA", "GO"), "5208707" 140 | ) 141 | self.assertEqual( 142 | get_code_by_municipality_name("Conceição do Coité", "BA"), "2908408" 143 | ) 144 | self.assertEqual( 145 | get_code_by_municipality_name("conceicao do Coite", "Ba"), "2908408" 146 | ) 147 | self.assertEqual( 148 | get_code_by_municipality_name("rio de janeiro", "rj"), "3304557" 149 | ) 150 | self.assertEqual( 151 | get_code_by_municipality_name("Lauro Müller", "sc"), "4209607" 152 | ) 153 | self.assertEqual( 154 | get_code_by_municipality_name("Tôrres", "rs"), "4321501" 155 | ) 156 | self.assertEqual( 157 | get_code_by_municipality_name("aurora", "ce"), "2301703" 158 | ) 159 | self.assertEqual( 160 | get_code_by_municipality_name("aurora", "sc"), "4201901" 161 | ) 162 | self.assertIsNone( 163 | get_code_by_municipality_name("Municipio Inexistente", "RS") 164 | ) 165 | self.assertIsNone( 166 | get_code_by_municipality_name("Municipio Inexistente", "") 167 | ) 168 | 169 | 170 | if __name__ == "__main__": 171 | main() 172 | -------------------------------------------------------------------------------- /tests/ibge/test_uf.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from brutils.ibge.uf import convert_code_to_uf 4 | 5 | 6 | class TestUF(TestCase): 7 | def test_convert_code_to_uf(self): 8 | # Testes para códigos válidos 9 | self.assertEqual(convert_code_to_uf("12"), "AC") 10 | self.assertEqual(convert_code_to_uf("33"), "RJ") 11 | self.assertEqual(convert_code_to_uf("31"), "MG") 12 | self.assertEqual(convert_code_to_uf("52"), "GO") 13 | 14 | # Testes para códigos inválidos 15 | self.assertIsNone(convert_code_to_uf("99")) 16 | self.assertIsNone(convert_code_to_uf("00")) 17 | self.assertIsNone(convert_code_to_uf("")) 18 | self.assertIsNone(convert_code_to_uf("AB")) 19 | -------------------------------------------------------------------------------- /tests/license_plate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brazilian-utils/brutils-python/c6cb92acfcfc81fe16e2d9aea8a58eb9689dce07/tests/license_plate/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/phone/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brazilian-utils/brutils-python/c6cb92acfcfc81fe16e2d9aea8a58eb9689dce07/tests/phone/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | remove_symbols, 13 | ) 14 | 15 | 16 | class TestCEP(TestCase): 17 | def test_remove_symbols(self): 18 | self.assertEqual(remove_symbols("00000000"), "00000000") 19 | self.assertEqual(remove_symbols("01310-200"), "01310200") 20 | self.assertEqual(remove_symbols("01..310.-200.-"), "01310200") 21 | self.assertEqual(remove_symbols("abc01310200*!*&#"), "abc01310200*!*&#") 22 | self.assertEqual( 23 | remove_symbols("ab.c1.--.3-102.-0-.0-.*.-!*&#"), "abc1310200*!*&#" 24 | ) 25 | self.assertEqual(remove_symbols("...---..."), "") 26 | 27 | def test_is_valid(self): 28 | # When CEP is not string, returns False 29 | self.assertIs(is_valid(1), False) 30 | 31 | # When CEP's len is different of 8, returns False 32 | self.assertIs(is_valid("1"), False) 33 | 34 | # When CEP does not contain only digits, returns False 35 | self.assertIs(is_valid("1234567-"), False) 36 | 37 | # When CEP is valid 38 | self.assertIs(is_valid("99999999"), True) 39 | self.assertIs(is_valid("88390000"), True) 40 | 41 | def test_generate(self): 42 | for _ in range(10_000): 43 | self.assertIs(is_valid(generate()), True) 44 | 45 | 46 | @patch("brutils.cep.is_valid") 47 | class TestIsValidToFormat(TestCase): 48 | def test_when_cep_is_valid_returns_True_to_format(self, mock_is_valid): 49 | mock_is_valid.return_value = True 50 | 51 | self.assertEqual(format_cep("01310200"), "01310-200") 52 | 53 | # Checks if function is_valid_cnpj is called 54 | mock_is_valid.assert_called_once_with("01310200") 55 | 56 | def test_when_cep_is_not_valid_returns_none(self, mock_is_valid): 57 | mock_is_valid.return_value = False 58 | 59 | # When cep isn't valid, returns None 60 | self.assertIsNone(format_cep("013102009")) 61 | 62 | 63 | @patch("brutils.cep.urlopen") 64 | class TestCEPAPICalls(TestCase): 65 | @patch("brutils.cep.loads") 66 | def test_get_address_from_cep_success(self, mock_loads, mock_urlopen): 67 | mock_loads.return_value = {"cep": "01310-200"} 68 | 69 | self.assertEqual( 70 | get_address_from_cep("01310200", True), {"cep": "01310-200"} 71 | ) 72 | 73 | def test_get_address_from_cep_raise_exception_invalid_cep( 74 | self, mock_urlopen 75 | ): 76 | mock_data = MagicMock() 77 | mock_data.read.return_value = {"erro": True} 78 | mock_urlopen.return_value = mock_data 79 | 80 | self.assertIsNone(get_address_from_cep("013102009")) 81 | 82 | def test_get_address_from_cep_invalid_cep_raise_exception_invalid_cep( 83 | self, mock_urlopen 84 | ): 85 | with self.assertRaises(InvalidCEP): 86 | get_address_from_cep("abcdef", True) 87 | 88 | def test_get_address_from_cep_invalid_cep_raise_exception_cep_not_found( 89 | self, mock_urlopen 90 | ): 91 | mock_data = MagicMock() 92 | mock_data.read.return_value = {"erro": True} 93 | mock_urlopen.return_value = mock_data 94 | 95 | with self.assertRaises(CEPNotFound): 96 | get_address_from_cep("01310209", True) 97 | 98 | @patch("brutils.cep.loads") 99 | def test_get_cep_information_from_address_success( 100 | self, mock_loads, mock_urlopen 101 | ): 102 | mock_loads.return_value = [{"cep": "01310-200"}] 103 | 104 | self.assertDictEqual( 105 | get_cep_information_from_address( 106 | "SP", "Example", "Rua Example", True 107 | )[0], 108 | {"cep": "01310-200"}, 109 | ) 110 | 111 | @patch("brutils.cep.loads") 112 | def test_get_cep_information_from_address_success_with_uf_conversion( 113 | self, mock_loads, mock_urlopen 114 | ): 115 | mock_loads.return_value = [{"cep": "01310-200"}] 116 | 117 | self.assertDictEqual( 118 | get_cep_information_from_address( 119 | "São Paulo", "Example", "Rua Example", True 120 | )[0], 121 | {"cep": "01310-200"}, 122 | ) 123 | 124 | @patch("brutils.cep.loads") 125 | def test_get_cep_information_from_address_empty_response( 126 | self, mock_loads, mock_urlopen 127 | ): 128 | mock_loads.return_value = [] 129 | 130 | self.assertIsNone( 131 | get_cep_information_from_address("SP", "Example", "Rua Example") 132 | ) 133 | 134 | @patch("brutils.cep.loads") 135 | def test_get_cep_information_from_address_raise_exception_invalid_cep( 136 | self, mock_loads, mock_urlopen 137 | ): 138 | mock_loads.return_value = {"erro": True} 139 | 140 | self.assertIsNone( 141 | get_cep_information_from_address("SP", "Example", "Rua Example") 142 | ) 143 | 144 | def test_get_cep_information_from_address_invalid_cep_dont_raise_exception_invalid_uf( 145 | self, mock_urlopen 146 | ): 147 | self.assertIsNone( 148 | get_cep_information_from_address("ABC", "Example", "Rua Example") 149 | ) 150 | 151 | def test_get_cep_information_from_address_invalid_cep_raise_exception_invalid_uf( 152 | self, mock_urlopen 153 | ): 154 | with self.assertRaises(ValueError): 155 | get_cep_information_from_address( 156 | "ABC", "Example", "Rua Example", True 157 | ) 158 | 159 | def test_get_cep_information_from_address_invalid_cep_raise_exception_cep_not_found( 160 | self, mock_urlopen 161 | ): 162 | mock_response = MagicMock() 163 | mock_response.read.return_value = {"erro": True} 164 | mock_urlopen.return_value = mock_response 165 | 166 | with self.assertRaises(CEPNotFound): 167 | get_cep_information_from_address( 168 | "SP", "Example", "Rua Example", True 169 | ) 170 | 171 | 172 | if __name__ == "__main__": 173 | main() 174 | -------------------------------------------------------------------------------- /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_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_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 | print(format_currency(0)) 19 | assert format_currency(0) == "R$ 0,00" 20 | 21 | def test_value_decimal_replace_rounding(self): 22 | assert format_currency(-123236.7676) == "R$ -123.236,77" 23 | 24 | def test_when_value_is_not_a_valid_currency(self): 25 | assert format_currency("not a number") is None 26 | assert format_currency("09809,87") is None 27 | assert format_currency("897L") is None 28 | 29 | 30 | class TestConvertRealToText(TestCase): 31 | def test_convert_real_to_text(self): 32 | self.assertEqual(convert_real_to_text(0.00), "Zero reais") 33 | self.assertEqual(convert_real_to_text(0.01), "Um centavo") 34 | self.assertEqual(convert_real_to_text(0.50), "Cinquenta centavos") 35 | self.assertEqual(convert_real_to_text(1.00), "Um real") 36 | self.assertEqual( 37 | convert_real_to_text(-50.25), 38 | "Menos cinquenta reais e vinte e cinco centavos", 39 | ) 40 | self.assertEqual( 41 | convert_real_to_text(1523.45), 42 | "Mil, quinhentos e vinte e três reais e quarenta e cinco centavos", 43 | ) 44 | self.assertEqual(convert_real_to_text(1000000.00), "Um milhão de reais") 45 | self.assertEqual( 46 | convert_real_to_text(2000000.00), "Dois milhões de reais" 47 | ) 48 | self.assertEqual( 49 | convert_real_to_text(1000000000.00), "Um bilhão de reais" 50 | ) 51 | self.assertEqual( 52 | convert_real_to_text(2000000000.00), "Dois bilhões de reais" 53 | ) 54 | self.assertEqual( 55 | convert_real_to_text(1000000000000.00), "Um trilhão de reais" 56 | ) 57 | self.assertEqual( 58 | convert_real_to_text(2000000000000.00), "Dois trilhões de reais" 59 | ) 60 | self.assertEqual( 61 | convert_real_to_text(1000000.45), 62 | "Um milhão de reais e quarenta e cinco centavos", 63 | ) 64 | self.assertEqual( 65 | convert_real_to_text(2000000000.99), 66 | "Dois bilhões de reais e noventa e nove centavos", 67 | ) 68 | self.assertEqual( 69 | convert_real_to_text(1234567890.50), 70 | "Um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa reais e cinquenta centavos", 71 | ) 72 | 73 | # Almost zero values 74 | self.assertEqual(convert_real_to_text(0.001), "Zero reais") 75 | self.assertEqual(convert_real_to_text(0.009), "Zero reais") 76 | 77 | # Negative milions 78 | self.assertEqual( 79 | convert_real_to_text(-1000000.00), "Menos um milhão de reais" 80 | ) 81 | self.assertEqual( 82 | convert_real_to_text(-2000000.50), 83 | "Menos dois milhões de reais e cinquenta centavos", 84 | ) 85 | 86 | # billions with cents 87 | self.assertEqual( 88 | convert_real_to_text(1000000000.01), 89 | "Um bilhão de reais e um centavo", 90 | ) 91 | self.assertEqual( 92 | convert_real_to_text(1000000000.99), 93 | "Um bilhão de reais e noventa e nove centavos", 94 | ) 95 | 96 | self.assertEqual( 97 | convert_real_to_text(999999999999.99), 98 | "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", 99 | ) 100 | 101 | # trillions with cents 102 | self.assertEqual( 103 | convert_real_to_text(1000000000000.01), 104 | "Um trilhão de reais e um centavo", 105 | ) 106 | self.assertEqual( 107 | convert_real_to_text(1000000000000.99), 108 | "Um trilhão de reais e noventa e nove centavos", 109 | ) 110 | self.assertEqual( 111 | convert_real_to_text(9999999999999.99), 112 | "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", 113 | ) 114 | 115 | # 1 quadrillion 116 | self.assertEqual( 117 | convert_real_to_text(1000000000000000.00), 118 | "Um quatrilhão de reais", 119 | ) 120 | 121 | # Edge cases should return None 122 | self.assertIsNone( 123 | convert_real_to_text("invalid_value") 124 | ) # invalid value 125 | self.assertIsNone(convert_real_to_text(None)) # None value 126 | self.assertIsNone( 127 | convert_real_to_text(-1000000000000001.00) 128 | ) # less than -1 quadrillion 129 | self.assertIsNone( 130 | convert_real_to_text(-1000000000000001.00) 131 | ) # more than 1 quadrillion 132 | self.assertIsNone(convert_real_to_text(float("inf"))) # Infinity 133 | self.assertIsNone( 134 | convert_real_to_text(float("nan")) 135 | ) # Not a number (NaN) 136 | -------------------------------------------------------------------------------- /tests/test_date.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from num2words import num2words 4 | 5 | from brutils import convert_date_to_text 6 | from brutils.data.enums.months import MonthsEnum 7 | 8 | 9 | class TestNum2Words(TestCase): 10 | def test_num_conversion(self) -> None: 11 | """ 12 | Smoke test of the num2words library. 13 | This test is used to guarantee that our dependency still works. 14 | """ 15 | self.assertEqual(num2words(30, lang="pt-br"), "trinta") 16 | self.assertEqual(num2words(42, lang="pt-br"), "quarenta e dois") 17 | self.assertEqual( 18 | num2words(2024, lang="pt-br"), "dois mil e vinte e quatro" 19 | ) 20 | self.assertEqual(num2words(0, lang="pt-br"), "zero") 21 | self.assertEqual(num2words(-1, lang="pt-br"), "menos um") 22 | 23 | 24 | class TestDate(TestCase): 25 | def test_convert_date_to_text(self): 26 | self.assertEqual( 27 | convert_date_to_text("15/08/2024"), 28 | "Quinze de agosto de dois mil e vinte e quatro", 29 | ) 30 | self.assertEqual( 31 | convert_date_to_text("01/01/2000"), 32 | "Primeiro de janeiro de dois mil", 33 | ) 34 | self.assertEqual( 35 | convert_date_to_text("31/12/1999"), 36 | "Trinta e um de dezembro de mil novecentos e noventa e nove", 37 | ) 38 | 39 | # 40 | self.assertIsNone(convert_date_to_text("30/02/2020"), None) 41 | self.assertIsNone(convert_date_to_text("30/00/2020"), None) 42 | self.assertIsNone(convert_date_to_text("30/02/2000"), None) 43 | self.assertIsNone(convert_date_to_text("50/09/2000"), None) 44 | self.assertIsNone(convert_date_to_text("25/15/2000"), None) 45 | self.assertIsNone(convert_date_to_text("29/02/2019"), None) 46 | 47 | # Invalid date pattern. 48 | self.assertRaises(ValueError, convert_date_to_text, "Invalid") 49 | self.assertRaises(ValueError, convert_date_to_text, "25/1/2020") 50 | self.assertRaises(ValueError, convert_date_to_text, "1924/08/20") 51 | self.assertRaises(ValueError, convert_date_to_text, "5/09/2020") 52 | 53 | self.assertEqual( 54 | convert_date_to_text("29/02/2020"), 55 | "Vinte e nove de fevereiro de dois mil e vinte", 56 | ) 57 | self.assertEqual( 58 | convert_date_to_text("01/01/1900"), 59 | "Primeiro de janeiro de mil e novecentos", 60 | ) 61 | 62 | months_year = [ 63 | (1, "janeiro"), 64 | (2, "fevereiro"), 65 | (3, "marco"), 66 | (4, "abril"), 67 | (5, "maio"), 68 | (6, "junho"), 69 | (7, "julho"), 70 | (8, "agosto"), 71 | (9, "setembro"), 72 | (10, "outubro"), 73 | (11, "novembro"), 74 | (12, "dezembro"), 75 | ] 76 | 77 | def testMonthEnum(self): 78 | for number_month, name_month in self.months_year: 79 | month = MonthsEnum(number_month) 80 | self.assertEqual(month.month_name, name_month) 81 | -------------------------------------------------------------------------------- /tests/test_date_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import TestCase 3 | 4 | from brutils.date_utils import is_holiday 5 | 6 | 7 | class TestIsHoliday(TestCase): 8 | def test_feriados_validos(self): 9 | # Testes com feriados válidos 10 | self.assertTrue(is_holiday(datetime(2024, 1, 1))) # Ano Novo 11 | self.assertTrue( 12 | is_holiday(datetime(2024, 7, 9), uf="SP") 13 | ) # Revolução Constitucionalista (SP) 14 | self.assertTrue( 15 | is_holiday(datetime(2024, 9, 7)) 16 | ) # Independência do Brasil 17 | self.assertTrue(is_holiday(datetime(2025, 1, 1))) # Ano Novo 18 | 19 | def test_dias_normais(self): 20 | # Testes com dias normais 21 | self.assertFalse(is_holiday(datetime(2024, 1, 2))) # Dia normal 22 | self.assertFalse( 23 | is_holiday(datetime(2024, 7, 9), uf="RJ") 24 | ) # Dia normal no RJ 25 | 26 | def test_data_invalida(self): 27 | # Testes com data inválida 28 | self.assertIsNone(is_holiday("2024-01-01")) # Formato incorreto 29 | self.assertIsNone(is_holiday(None)) # Data None 30 | 31 | def test_uf_invalida(self): 32 | # Testes com UF inválida 33 | self.assertIsNone( 34 | is_holiday(datetime(2024, 1, 1), uf="XX") 35 | ) # UF inválida 36 | self.assertIsNone( 37 | is_holiday(datetime(2024, 1, 1), uf="SS") 38 | ) # UF inválida 39 | 40 | def test_limite_de_datas(self): 41 | # Testes com limite de datas 42 | self.assertTrue(is_holiday(datetime(2024, 12, 25))) # Natal 43 | self.assertTrue( 44 | is_holiday(datetime(2024, 11, 15)) 45 | ) # Proclamação da República 46 | 47 | def test_datas_depois_de_feriados(self): 48 | # Test data after holidays 49 | self.assertFalse(is_holiday(datetime(2024, 12, 26))) # Não é feriado 50 | self.assertFalse(is_holiday(datetime(2025, 1, 2))) # Não é feriado 51 | 52 | def test_ano_bissexto(self): 53 | # Teste ano bissexto 54 | self.assertFalse( 55 | is_holiday(datetime(2024, 2, 29)) 56 | ) # Não é feriado, mas data válida 57 | # Uncomment to test non-leap year invalid date 58 | # self.assertIsNone(is_holiday(datetime(1900, 2, 29))) # Ano não bissexto, data inválida 59 | 60 | def test_data_passada_futura(self): 61 | # Teste de data passada e futura 62 | self.assertTrue(is_holiday(datetime(2023, 1, 1))) # Ano anterior 63 | self.assertTrue(is_holiday(datetime(2100, 12, 25))) # Ano futuro 64 | self.assertFalse( 65 | is_holiday(datetime(2100, 1, 2)) 66 | ) # Dia normal em ano futuro 67 | 68 | def test_data_sem_uf(self): 69 | # Teste feriado nacional sem UF 70 | self.assertTrue( 71 | is_holiday(datetime(2024, 12, 25)) 72 | ) # Natal, feriado nacional 73 | self.assertFalse( 74 | is_holiday(datetime(2024, 7, 9)) 75 | ) # Data estadual de SP, sem UF 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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.assertIsNone(format_legal_process("2314194582005507")) 24 | self.assertIsNone(format_legal_process("0000000000000000000000000")) 25 | self.assertIsNone(format_legal_process("0000000000000000000asdasd")) 26 | 27 | def test_remove_symbols(self): 28 | self.assertEqual( 29 | remove_symbols("6439067-89.2023.4.04.5902"), "64390678920234045902" 30 | ) 31 | self.assertEqual( 32 | remove_symbols("4976023-82.2012.7.00.2263"), "49760238220127002263" 33 | ) 34 | self.assertEqual( 35 | remove_symbols("4976...-02382-.-2012.-7002--263"), 36 | "49760238220127002263", 37 | ) 38 | self.assertEqual( 39 | remove_symbols("4976023-82.2012.7.00.2263*!*&#"), 40 | "49760238220127002263*!*&#", 41 | ) 42 | self.assertEqual( 43 | remove_symbols("4976..#.-0@2382-.#-2012.#-7002--263@"), 44 | "4976#0@2382#2012#7002263@", 45 | ) 46 | self.assertEqual(remove_symbols("@...---...#"), "@#") 47 | self.assertEqual(remove_symbols("...---..."), "") 48 | 49 | def test_generate(self): 50 | self.assertEqual(generate()[9:13], str(datetime.now().year)) 51 | self.assertEqual(generate(year=3000)[9:13], "3000") 52 | self.assertEqual(generate(orgao=4)[13:14], "4") 53 | self.assertEqual(generate(year=3000, orgao=4)[9:13], "3000") 54 | self.assertIsNone(generate(year=1000, orgao=4)) 55 | self.assertIsNone(generate(orgao=0)) 56 | 57 | def test_check_sum(self): 58 | self.assertEqual(_checksum(546611720238150014), "77") 59 | self.assertEqual(_checksum(403818720238230498), "50") 60 | 61 | def test_is_valid(self): 62 | self.assertIs(is_valid("10188748220234018200"), True) 63 | self.assertIs(is_valid("45532346920234025107"), True) 64 | self.assertIs(is_valid("10188748220239918200"), False) 65 | self.assertIs(is_valid("00000000000000000000"), False) 66 | self.assertIs(is_valid("455323469202340251"), False) 67 | self.assertIs(is_valid("455323469202340257123123123"), False) 68 | self.assertIs(is_valid("455323423QQWEQWSsasd&*(()"), False) 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /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_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 | 22 | def test_invalid_voter_id(self): 23 | # test an invalid voter id number (dv1 & UF fail) 24 | voter_id = "123456789011" 25 | self.assertIs(is_valid(voter_id), False) 26 | 27 | def test_invalid_length(self): 28 | # Test an invalid length for voter id 29 | invalid_length_short = "12345678901" 30 | invalid_length_long = "1234567890123" 31 | self.assertIs(is_valid(invalid_length_short), False) 32 | self.assertIs(is_valid(invalid_length_long), False) 33 | 34 | def test_invalid_characters(self): 35 | # Test voter id with non-numeric characters 36 | invalid_characters = "ABCD56789012" 37 | invalid_characters_space = "217633 460 930" 38 | self.assertIs(is_valid(invalid_characters), False) 39 | self.assertIs(is_valid(invalid_characters_space), False) 40 | 41 | def test_valid_special_case(self): 42 | # Test a valid edge case (SP & MG with 13 digits) 43 | valid_special = "3244567800167" 44 | self.assertIs(is_valid(valid_special), True) 45 | 46 | def test_invalid_vd1(self): 47 | voter_id = "427503840223" 48 | self.assertIs(is_valid(voter_id), False) 49 | 50 | def test_invalid_vd2(self): 51 | voter_id = "427503840214" 52 | self.assertIs(is_valid(voter_id), False) 53 | 54 | def test_get_voter_id_parts(self): 55 | voter_id = "12345678AB12" 56 | 57 | sequential_number = _get_sequential_number(voter_id) 58 | federative_union = _get_federative_union(voter_id) 59 | verifying_digits = _get_verifying_digits(voter_id) 60 | 61 | self.assertEqual(sequential_number, "12345678") 62 | self.assertEqual(federative_union, "AB") 63 | self.assertEqual(verifying_digits, "12") 64 | 65 | def test_valid_length_verify(self): 66 | voter_id = "123456789012" 67 | self.assertIs(_is_length_valid(voter_id), True) 68 | 69 | def test_invalid_length_verify(self): 70 | voter_id = "12345678AB123" # Invalid length 71 | self.assertIs(_is_length_valid(voter_id), False) 72 | 73 | def test_calculate_vd1(self): 74 | self.assertIs(_calculate_vd1("07881476", "03"), 6) 75 | 76 | # test edge case: when federative union is SP and rest is 0, declare vd1 as 1 77 | self.assertIs(_calculate_vd1("73146499", "01"), 1) 78 | # test edge case: when federative union is MG and rest is 0, declare vd1 as 1 79 | self.assertIs(_calculate_vd1("42750359", "02"), 1) 80 | # test edge case: rest is 10, declare vd1 as 0 81 | self.assertIs(_calculate_vd1("73146415", "03"), 0) 82 | 83 | def test_calculate_vd2(self): 84 | self.assertIs(_calculate_vd2("02", 7), 2) 85 | # edge case: if rest == 10, declare vd2 as zero 86 | self.assertIs(_calculate_vd2("03", 7), 0) 87 | # edge case: if UF is "01" (for SP) and rest == 0 88 | # declare dv2 as 1 instead 89 | self.assertIs(_calculate_vd2("01", 4), 1) 90 | # edge case: if UF is "02" (for MG) and rest == 0 91 | # declare dv2 as 1 instead 92 | self.assertIs(_calculate_vd2("02", 8), 1) 93 | 94 | def test_generate_voter_id(self): 95 | # test if is_valid a voter id from MG 96 | voter_id = generate(federative_union="MG") 97 | self.assertIs(is_valid(voter_id), True) 98 | 99 | # test if is_valid a voter id from AC 100 | voter_id = generate(federative_union="AC") 101 | self.assertIs(is_valid(voter_id), True) 102 | 103 | # test if is_valid a voter id from foreigner 104 | voter_id = generate() 105 | self.assertIs(is_valid(voter_id), True) 106 | 107 | # test if UF is not valid 108 | voter_id = generate(federative_union="XX") 109 | self.assertIs(is_valid(voter_id), False) 110 | 111 | def test_format_voter_id(self): 112 | self.assertEqual(format_voter_id("277627122852"), "2776 2712 28 52") 113 | self.assertIsNone(format_voter_id("00000000000")) 114 | self.assertIsNone(format_voter_id("0000000000a")) 115 | self.assertIsNone(format_voter_id("000000000000")) 116 | self.assertIsNone(format_voter_id("800911840197")) 117 | 118 | 119 | if __name__ == "__main__": 120 | main() 121 | --------------------------------------------------------------------------------