├── .flake8 ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── examples ├── cepform │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── cep-form.html │ ├── tests.py │ └── views.py ├── examples_app │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── requirements.txt ├── js ├── jest.config.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src │ ├── fields-finder.test.ts │ ├── fields-finder.ts │ ├── handlers │ │ ├── cep-input.test.ts │ │ ├── cepCleaner.ts │ │ ├── cepFetcher.ts │ │ ├── cepFieldsAutoFocus.ts │ │ ├── cepFieldsFiller.ts │ │ ├── cepFieldsLocker.ts │ │ ├── cepLoadingIndicator.ts │ │ ├── cepValidator.ts │ │ └── index.ts │ ├── index.ts │ ├── install-handlers.test.ts │ ├── install-handlers.ts │ ├── quick-events.ts │ └── types.ts └── tsconfig.json ├── manage.py ├── mypy.ini ├── setup.py ├── simplecep ├── __init__.py ├── cache │ ├── __init__.py │ └── db_cache.py ├── conf.py ├── core.py ├── fetcher.py ├── fields.py ├── locale │ └── pt_BR │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── providers │ ├── __init__.py │ ├── base.py │ ├── default │ │ ├── __init__.py │ │ ├── correios_sigep.py │ │ ├── republicavirtual.py │ │ └── viacep.py │ ├── fetcher.py │ └── get_installed.py ├── static │ └── simplecep │ │ └── simplecep-autofill.js ├── templates │ └── simplecep │ │ └── widgets │ │ ├── cep-loading-indicator.html │ │ └── cep.html ├── urls.py └── views.py ├── test-manage.py ├── tests ├── .coveragerc ├── __init__.py ├── empty_urls.py ├── providers │ ├── __init__.py │ ├── capture_real_responses.py │ ├── captured_responses.py │ ├── providers_tests_data.py │ ├── test_providers_expected_responses.py │ └── test_providers_unexpected_responses.py ├── settings.py ├── test_cep_db_cache.py ├── test_core.py ├── test_fetcher.py ├── test_form.py ├── test_providers.py ├── test_settings.py ├── test_view.py ├── urls.py └── utils.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | select = C,E,F,W,B,B950 4 | ignore = W292,W504,E501,W503,E303,W293,E122,E231,E271,E222,E261,B950 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | *.py[cod] 4 | *.so 5 | 6 | # Distribution / packaging 7 | *.egg-info/ 8 | build/ 9 | dist/ 10 | 11 | # Unit test / coverage reports 12 | .tox/ 13 | tests/coverage_html/ 14 | tests/.coverage 15 | 16 | # mypy 17 | .mypy_cache/ 18 | 19 | # IDEs 20 | .idea 21 | .spyderproject 22 | .spyproject 23 | .ropeproject 24 | 25 | # js 26 | node_modules 27 | .rpt2_cache 28 | js/dist 29 | coverage 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cauê Thenório 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include *.md 4 | graft simplecep 5 | global-exclude __pycache__ 6 | global-exclude *.py[co] 7 | global-exclude .DS_Store 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint black black-list test coverage-report test-all messages compilemessages migrations 2 | 3 | lint: ## check style with flake8 4 | flake8 simplecep 5 | 6 | black: ## reformat code with black 7 | black simplecep 8 | 9 | black-lint: black lint 10 | 11 | test: ## run tests quickly with the default Python 12 | cd tests && coverage run ../manage.py test 13 | 14 | coverage-report: 15 | cd tests && coverage html && open ./coverage_html/index.html 16 | 17 | test-all: ## run tests on every Python version with tox 18 | tox 19 | 20 | messages: ## generate brazilian portuguese po file for translation 21 | cd simplecep && python ../manage.py makemessages --locale pt_BR 22 | 23 | compilemessages: ## compile translation po file to binary mo file 24 | cd simplecep && python ../manage.py compilemessages 25 | 26 | migrations: ## build simplecep migration files 27 | python manage.py makemigrations simplecep 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-simplecep 2 | 3 | Django-simplecep helps you to fetch Brazilian address data associated to a CEP 4 | value. It also auto-populates form address-fields based on provided CEP. 5 | 6 | Example: 7 | [image] 8 | 9 | ## How it works 10 | 11 | The library uses the configured data providers to fetch the CEP data. 12 | Each installed provider is used until the data is successfully fetched. 13 | 14 | By default it will use as data-provider, in order: 15 | 1. Official Correios API (SIGEP WEB) 16 | 2. www.republicavirtual.com.br API 17 | 3. viacep.com.br API 18 | 19 | After fetch, the retrieved data is cached (by default on a Django model) 20 | so any data-retrieval attempt for the same CEP is resolved much faster and 21 | do not use external providers. 22 | 23 | The providers and the cache mechanism can be fully customized by the user. 24 | 25 | If the CEP data isn't cached and all providers fail, 26 | the `NoAvailableCepProviders` exception is raised. 27 | 28 | ## Usage 29 | 30 | This library can be used in three different ways: 31 | 32 | ### As Python function 33 | 34 | ```python 35 | >>> from simplecep.fetcher import get_cep_data 36 | >>> cep_data = get_cep_data('59615350') 37 | 38 | >>> cep_data 39 | 40 | 41 | >>> cep_data.to_dict() 42 | {'cep': '59615350', 'street': 'Rua João Simão do Nascimento', 'state': 'RN', 'district': 'Santa Delmira', 'city': 'Mossoró'} 43 | ``` 44 | 45 | ### As Django app API endpoint 46 | 47 | You can include the `simplecep` URLconf into your Django project URLs to be 48 | able to query CEP data using HTTP requests: 49 | 50 | ```shell 51 | curl "http://localhost:8000/cep/59615350/" 52 | {"cep": "59615350", "street": "Rua João Simão do Nascimento", "state": "RN", "district": "Santa Delmira", "city": "Mossoró"} 53 | ``` 54 | 55 | ### As Django form field 56 | 57 | The library provides the `CepField` form field which auto-populate other 58 | address fields in the form when a CEP is typed by the user. 59 | 60 | ```python 61 | from simplecep import CEPField 62 | 63 | class CepForm(forms.Form): 64 | cep = CEPField( 65 | label='Your CEP', 66 | autofill={ 67 | # key is the data-type and value is the form-field name 68 | "district": "bairro", 69 | "state": "estado", 70 | "city": "cidade", 71 | "street": "rua", 72 | "street_number": "numbero_rua", 73 | } 74 | ) 75 | estado = forms.CharField() 76 | cidade = forms.CharField() 77 | bairro = forms.CharField() 78 | rua = forms.CharField() 79 | numbero_rua = forms.CharField() 80 | ``` 81 | 82 | The `CepField` form field uses Javascript (no jquery) to retrieve the data 83 | from the CEP endpoint available on your app. It can be totally customized. 84 | 85 | ## Installation 86 | 87 | 1. Install the `django-simplecep` package using `pip` or your favourite python 88 | package-manager: 89 | 90 | ```shell 91 | pip install django-cep 92 | ``` 93 | 94 | 2. Adds `simplecep` to your Django `INSTALLED_APPS` setting: 95 | 96 | ```python 97 | INSTALLED_APPS = ( 98 | # your other installed apps here 99 | # ... 100 | 'simplecep', 101 | ) 102 | ``` 103 | 104 | 3. Run Django `migrate` command to create the CEP cache table: 105 | 106 | ```shell 107 | python manage.py migrate 108 | ``` 109 | 110 | 4. Include the `simplecep` URLconf in your project `urls.py`: 111 | 112 | ```python 113 | urlpatterns = [ 114 | # your app paths here 115 | path('cep/', include('simplecep.urls')), 116 | ] 117 | ``` 118 | 119 | This step is optional and can be skipped if you won't use the CEP API 120 | endpoint or the `CepField` form field. 121 | 122 | ## Configuration 123 | 124 | The library tries to use sane default-values, so you can use it without any additional 125 | configuration, but you can configure it adding a `SIMPLECEP` object to your 126 | Django settings file. 127 | 128 | Here's the default configuration: 129 | 130 | ```python 131 | SIMPLECEP = { 132 | # a list of CEP providers modules - you can create your own 133 | "PROVIDERS": ( 134 | "simplecep.providers.CorreiosSIGEPCEPProvider", 135 | "simplecep.providers.RepublicaVirtualCEPProvider", 136 | "simplecep.providers.ViaCEPProvider", 137 | ), 138 | # the cache class - it should implement dict methods 139 | "CACHE": "simplecep.cache.CepDatabaseCache", 140 | # max-time to wait for each provider response in secs 141 | "PROVIDERS_TIMEOUT": 2, 142 | # time to keep the cached data - 6 months by default 143 | "CEP_CACHE_MAXAGE": datetime.timedelta(days=30 * 6), 144 | } 145 | ``` 146 | ## Customizing the CepField JS behaviour 147 | 148 | By default the Javascript code included by the `CepField`: 149 | - Adds a mask to the CEP field so value format is always 99999-999 150 | - Fetches the CEP data using the browser `fetch` function 151 | - Shows a loading indicator in the CEP field while data is being fetched 152 | - Makes address fields `readonly` while the data is being fetched 153 | - Populate address fields with obtained CEP data 154 | - Focus on the next not-populated address field after populating the fields 155 | 156 | Some behaviours above can be customized using `CepField` parameters. 157 | All behaviours can be customized writing some JS code. 158 | 159 | ## Requirements 160 | 161 | * Django 2.0 or higher 162 | * Python 3.6 or higher 163 | -------------------------------------------------------------------------------- /examples/cepform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cauethenorio/django-simplecep/acab8a99fe3df8c6a2f01909c07fa36b1ea2d922/examples/cepform/__init__.py -------------------------------------------------------------------------------- /examples/cepform/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/cepform/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CepformConfig(AppConfig): 5 | name = "cepform" 6 | -------------------------------------------------------------------------------- /examples/cepform/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from simplecep import CEPField 4 | 5 | 6 | class CepForm(forms.Form): 7 | cep = CEPField( 8 | autofill={ 9 | "district": "district", 10 | "state": "state", 11 | "city": "city", 12 | "street": "street", 13 | "street_number": "street_number", 14 | } 15 | ) 16 | state = forms.CharField() 17 | city = forms.CharField() 18 | district = forms.CharField() 19 | street = forms.CharField() 20 | street_number = forms.CharField() 21 | -------------------------------------------------------------------------------- /examples/cepform/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cauethenorio/django-simplecep/acab8a99fe3df8c6a2f01909c07fa36b1ea2d922/examples/cepform/migrations/__init__.py -------------------------------------------------------------------------------- /examples/cepform/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/cepform/templates/cep-form.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Django SimpleCEP Example 9 | 10 | 11 |
12 |
13 |
14 |

Django SimpleCEP Example

15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | {{ form.media }} 19 | 20 |
21 |
22 | {% csrf_token %} 23 | {{ form2|crispy }} 24 | {{ form2.media }} 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/cepform/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/cepform/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic.edit import FormView 3 | 4 | from .forms import CepForm 5 | 6 | 7 | class CepFormView(FormView): 8 | template_name = "cep-form.html" 9 | form_class = CepForm 10 | 11 | def get_context_data(self, **kwargs): 12 | context = super().get_context_data(**kwargs) 13 | context["form2"] = CepForm(prefix="2") 14 | return context 15 | -------------------------------------------------------------------------------- /examples/examples_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cauethenorio/django-simplecep/acab8a99fe3df8c6a2f01909c07fa36b1ea2d922/examples/examples_app/__init__.py -------------------------------------------------------------------------------- /examples/examples_app/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for examples_app project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "c7t*%#i&y#m*$6c8fig+dnd4)!_%d6@xg7_yhmeggu^x5_t6no" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | # 3rd party 41 | "crispy_forms", 42 | "simplecep", 43 | # local apps 44 | "cepform", 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | "django.middleware.security.SecurityMiddleware", 49 | "django.contrib.sessions.middleware.SessionMiddleware", 50 | "django.middleware.common.CommonMiddleware", 51 | "django.middleware.csrf.CsrfViewMiddleware", 52 | "django.contrib.auth.middleware.AuthenticationMiddleware", 53 | "django.contrib.messages.middleware.MessageMiddleware", 54 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 55 | ] 56 | 57 | ROOT_URLCONF = "examples_app.urls" 58 | 59 | TEMPLATES = [ 60 | { 61 | "BACKEND": "django.template.backends.django.DjangoTemplates", 62 | "DIRS": [], 63 | "APP_DIRS": True, 64 | "OPTIONS": { 65 | "context_processors": [ 66 | "django.template.context_processors.debug", 67 | "django.template.context_processors.request", 68 | "django.contrib.auth.context_processors.auth", 69 | "django.contrib.messages.context_processors.messages", 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = "examples_app.wsgi.application" 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 80 | 81 | DATABASES = { 82 | "default": { 83 | "ENGINE": "django.db.backends.sqlite3", 84 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 95 | }, 96 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, 97 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, 98 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, 99 | ] 100 | 101 | 102 | # Internationalization 103 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 104 | 105 | LANGUAGE_CODE = "pt-br" 106 | 107 | TIME_ZONE = "UTC" 108 | 109 | USE_I18N = True 110 | 111 | USE_L10N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 118 | 119 | STATIC_URL = "/static/" 120 | 121 | CRISPY_TEMPLATE_PACK = "bootstrap4" 122 | -------------------------------------------------------------------------------- /examples/examples_app/urls.py: -------------------------------------------------------------------------------- 1 | """examples_app URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.urls import path, include 19 | 20 | 21 | from simplecep import urls 22 | 23 | from cepform.views import CepFormView 24 | 25 | urlpatterns = [path("", CepFormView.as_view()), path("cep/", include(urls))] + static( 26 | settings.STATIC_URL, document_root=settings.STATIC_ROOT 27 | ) 28 | -------------------------------------------------------------------------------- /examples/examples_app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for examples_app project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'examples_app.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'examples_app.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.2.18 2 | django-crispy-forms==1.8.0 3 | -------------------------------------------------------------------------------- /js/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | roots: ["/src"], 5 | collectCoverage: true, 6 | collectCoverageFrom: [ 7 | "/src/**/*.ts", 8 | "!/src/**/types.ts", 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-simple-cep", 3 | "version": "0.1.0", 4 | "description": "Auto-fill brazilian address forms using CEP data", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup --config", 8 | "watch": "rollup --config --configDebug --watch", 9 | "test": "jest", 10 | "prettier": "prettier --write \"src/**/*.ts\"" 11 | }, 12 | "author": "Cauê Thenório ", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/jest": "^25.2.1", 16 | "jest": "^25.5.3", 17 | "prettier": "^2.0.5", 18 | "rollup": "^2.7.6", 19 | "rollup-plugin-terser": "^5.3.0", 20 | "rollup-plugin-typescript2": "^0.27.0", 21 | "ts-jest": "^25.4.0", 22 | "typescript": "^3.8.3" 23 | }, 24 | "dependencies": {} 25 | } 26 | -------------------------------------------------------------------------------- /js/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 99, 3 | tabWidth: 4, 4 | bracketSpacing: false, 5 | endOfLine: "lf" 6 | }; 7 | -------------------------------------------------------------------------------- /js/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import {terser} from "rollup-plugin-terser"; 3 | 4 | 5 | export default (commandLineArgs) => { 6 | const debug = commandLineArgs.configDebug === true; 7 | 8 | return ({ 9 | input: 'src/index.ts', 10 | output: { 11 | file: '../simplecep/static/simplecep/simplecep-autofill.js', 12 | format: 'iife', 13 | name: 'SimplecepAutofill', 14 | }, 15 | plugins: [ 16 | typescript({ 17 | typescript: require('typescript'), 18 | }), 19 | debug? undefined : terser() 20 | ] 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /js/src/fields-finder.test.ts: -------------------------------------------------------------------------------- 1 | import {querySimplecepAutofillFields} from "./fields-finder"; 2 | 3 | describe("querySimplecepAutofillFields", () => { 4 | it("should find a single field and extract its data", () => { 5 | document.body.innerHTML = ` 6 |

7 | 8 | 17 |

18 |

19 | 20 | 21 |

22 | `; 23 | 24 | expect(querySimplecepAutofillFields()).toEqual([ 25 | { 26 | cepField: document.getElementById("crazy_field_id"), 27 | baseCepURL: "/my-cep-route/00000000/", 28 | dataFields: [{type: "address", selector: "#my_custom_id"}] 29 | } 30 | ]); 31 | }); 32 | 33 | it("should find multiple fields and extract their data", () => { 34 | document.body.innerHTML = ` 35 |

36 | 45 |

46 |

47 | 52 |

`; 53 | 54 | expect(querySimplecepAutofillFields()).toEqual([ 55 | { 56 | cepField: document.getElementById("first_one"), 57 | baseCepURL: "/my-cep-route/00000000/", 58 | dataFields: [{type: "state", selector: "#id_estado"}] 59 | }, 60 | { 61 | cepField: document.getElementById("2nd_field"), 62 | baseCepURL: "/another-cep-route/00000000/", 63 | dataFields: [ 64 | {type: "state", selector: "#id_statez"}, 65 | {type: "city", selector: "#id_cityz"}, 66 | {type: "district", selector: "#id_districtz"}, 67 | {type: "address", selector: "#id_addressz"} 68 | ] 69 | } 70 | ]); 71 | }); 72 | 73 | it("should remove the node attribute after getting the data", () => { 74 | document.body.innerHTML = ``; 75 | expect(querySimplecepAutofillFields()).toHaveLength(1); 76 | expect(querySimplecepAutofillFields()).toHaveLength(0); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /js/src/fields-finder.ts: -------------------------------------------------------------------------------- 1 | import {AutofillFieldDataType} from "./types"; 2 | 3 | export function querySimplecepAutofillFields(): Array { 4 | const selector = "[data-simplecep-autofill]"; 5 | let fields: Array = []; 6 | 7 | document.querySelectorAll(selector).forEach((cepField: HTMLInputElement) => { 8 | try { 9 | const autoFill: AutofillFieldDataType = JSON.parse(cepField.dataset.simplecepAutofill); 10 | 11 | // delete the attr to avoid adding the same handler multiple times 12 | // when there are more than one form on the page 13 | delete cepField.dataset.simplecepAutofill; 14 | 15 | const {baseCepURL, dataFields} = autoFill; 16 | fields.push({cepField, baseCepURL, dataFields}); 17 | } catch {} 18 | }); 19 | 20 | return fields; 21 | } 22 | -------------------------------------------------------------------------------- /js/src/handlers/cep-input.test.ts: -------------------------------------------------------------------------------- 1 | import {CepEvents} from "../types"; 2 | import {installHandlers} from "../install-handlers"; 3 | import {cepInputHandler} from "./cep-input"; 4 | 5 | function setupHandler( 6 | value: string, 7 | invalidCepListener: (e: CustomEvent) => void, 8 | validCepListener: (e: CustomEvent) => void 9 | ) { 10 | var node = document.createElement("input"); 11 | node.setAttribute("type", "text"); 12 | node.value = value; 13 | 14 | installHandlers( 15 | { 16 | cepField: node, 17 | baseCepURL: "", 18 | dataFields: [] 19 | }, 20 | [cepInputHandler] 21 | ); 22 | 23 | node.addEventListener(CepEvents.InvalidCepInput, invalidCepListener); 24 | node.addEventListener(CepEvents.ValidCepInput, validCepListener); 25 | 26 | node.dispatchEvent( 27 | new Event("input", { 28 | bubbles: true, 29 | cancelable: true 30 | }) 31 | ); 32 | 33 | return { 34 | invalidCepListener, 35 | validCepListener 36 | }; 37 | } 38 | 39 | describe("cepInputHandler", () => { 40 | it.each(["000000", "", "1234567a", "aaaa", "12345_678", "111112222"])( 41 | 'should trigger CepEvents.InvalidCepInput for invalid CEP input "%s"', 42 | (value: string) => { 43 | const listeners = setupHandler( 44 | value, 45 | jest.fn((e: CustomEvent) => expect(e.detail).toBe(value)), 46 | jest.fn() 47 | ); 48 | 49 | expect(listeners.invalidCepListener).toBeCalled(); 50 | expect(listeners.validCepListener).not.toBeCalled(); 51 | } 52 | ); 53 | 54 | it.each([ 55 | ["12345678", "12345678"], 56 | ["11111 111", "11111111"], 57 | ["18170-000", "18170000"] 58 | ])( 59 | 'should trigger CepEvents.ValidCepInput for valid CEP input "%s"', 60 | (value: string, formattedCep: string) => { 61 | const listeners = setupHandler( 62 | value, 63 | jest.fn(), 64 | jest.fn((e: CustomEvent) => expect(e.detail).toBe(formattedCep)) 65 | ); 66 | 67 | expect(listeners.validCepListener).toBeCalled(); 68 | expect(listeners.invalidCepListener).not.toBeCalled(); 69 | } 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /js/src/handlers/cepCleaner.ts: -------------------------------------------------------------------------------- 1 | import {HandlerParams, CepEvents} from "../types"; 2 | 3 | /* 4 | This handler is responsible for cleaning the values user inputs on the CEP field. 5 | 6 | It listens for 'input' event on the CEP input field and dispatches: 7 | - CepEvents.CEPValueCleaned 8 | With the cleaned value after the user changes the input value 9 | */ 10 | 11 | export const cepCleanerInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 12 | detail: { 13 | handlerName: "cepCleaner", 14 | installer: cepMaskInstaller, 15 | }, 16 | }); 17 | 18 | function cepMaskInstaller({fieldData, quickDispatchEvent, quickAddEventListener}: HandlerParams) { 19 | return quickAddEventListener("input", (_, e) => { 20 | if (e.target instanceof HTMLInputElement) { 21 | const {value} = e.target; 22 | 23 | let [formatted, start, end] = format(e.target); 24 | let selectionDelta = 0; 25 | 26 | if (formatted.length > 5) { 27 | formatted = `${formatted.substr(0, 5)}-${formatted.substr(5, 3)}`; 28 | if (start > 5) { 29 | selectionDelta += 1; 30 | } 31 | } 32 | 33 | e.target.value = formatted; 34 | e.target.selectionStart = Math.max(start + selectionDelta, 0); 35 | e.target.selectionEnd = Math.max(end + selectionDelta, 0); 36 | quickDispatchEvent(CepEvents.CEPValueCleaned, formatted); 37 | } 38 | }); 39 | } 40 | 41 | const clean = (value: string): string => value.replace(/\D/g, ""); 42 | 43 | const format = (el: HTMLInputElement): [string, number, number] => { 44 | const [start, end] = [el.selectionStart, el.selectionEnd].map((i) => { 45 | const cleaned = clean(el.value.slice(0, i)); 46 | return i + (cleaned.length - i); 47 | }); 48 | return [clean(el.value), start, end]; 49 | }; 50 | -------------------------------------------------------------------------------- /js/src/handlers/cepFetcher.ts: -------------------------------------------------------------------------------- 1 | import {CepEvents, DataFieldType, HandlerParams} from "../types"; 2 | 3 | type CepDataType = {[key in DataFieldType]: string | null}; 4 | 5 | /* 6 | This handler is responsible for fetching CEP data when a valid CEP is input. 7 | 8 | It listens for CepEvents.ValidCepInput events and dispatches: 9 | - CepEvents.CepFetchStart 10 | With the cepURL when it request for CEP data 11 | 12 | - CepEvents.CepFetchSuccess 13 | With fetched CEP data on success 14 | 15 | - CepEvents.CepFetchError 16 | With the error when it fails fetching the data 17 | 18 | - CepEvents.CepFetchFinish 19 | With CEP data or error, when the request is finished (with error or not). 20 | */ 21 | 22 | export const cepFetcherInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 23 | detail: { 24 | handlerName: "cepFetcher", 25 | installer: cepFetcherInstaller, 26 | }, 27 | }); 28 | 29 | function cepFetcherInstaller({ 30 | quickDispatchEvent, 31 | quickAddEventListener, 32 | getCepURL, 33 | }: HandlerParams) { 34 | return quickAddEventListener(CepEvents.ValidCepInput, (cep: string) => { 35 | const cepURL = getCepURL(cep); 36 | quickDispatchEvent(CepEvents.CepFetchStart, cepURL); 37 | 38 | fetchCepData(cepURL) 39 | .then( 40 | (response) => quickDispatchEvent(CepEvents.CepFetchSuccess, response), 41 | (error) => quickDispatchEvent(CepEvents.CepFetchError, error) 42 | ) 43 | .then((value) => quickDispatchEvent(CepEvents.CepFetchFinish, value)); 44 | }); 45 | } 46 | 47 | const fetchCepData = (url: string): Promise => 48 | fetch(url).then((response) => { 49 | if (response.status >= 200 && response.status < 300) { 50 | return response.json(); 51 | } else { 52 | var error: any = new Error(response.statusText || response.status.toString()); 53 | error.response = response; 54 | return Promise.reject(error); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /js/src/handlers/cepFieldsAutoFocus.ts: -------------------------------------------------------------------------------- 1 | import {CepEvents, HandlerParams} from "../types"; 2 | 3 | export const cepFieldsAutoFocusInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 4 | detail: { 5 | handlerName: "focus-next", 6 | installer: cepFieldsAutoFocusInstaller, 7 | }, 8 | }); 9 | 10 | function cepFieldsAutoFocusInstaller({quickAddEventListener}: HandlerParams) { 11 | return quickAddEventListener(CepEvents.CepFieldsAutofilled, ({fields, cepData}) => { 12 | for (const {type, els} of fields) { 13 | // search for the first field which returned with no data 14 | if (cepData[type] == null) { 15 | for (const el of els) { 16 | // search for the first element which is a form field 17 | // attached to the field type 18 | if (formFieldsTags.indexOf(el.tagName) >= 0) { 19 | el.focus(); 20 | return; 21 | } 22 | } 23 | } 24 | } 25 | }); 26 | } 27 | 28 | const formFieldsTags = ["INPUT", "SELECT", "TEXTAREA"]; 29 | -------------------------------------------------------------------------------- /js/src/handlers/cepFieldsFiller.ts: -------------------------------------------------------------------------------- 1 | import {CepEvents, HandlerParams} from "../types"; 2 | 3 | export const cepFieldsFillerInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 4 | detail: { 5 | handlerName: "cepFieldsFiller", 6 | installer: cepFieldsFillerInstaller, 7 | }, 8 | }); 9 | 10 | function cepFieldsFillerInstaller({ 11 | getDataFields, 12 | quickAddEventListener, 13 | quickDispatchEvent, 14 | }: HandlerParams) { 15 | return quickAddEventListener(CepEvents.CepFetchSuccess, (cepData: CepDataType) => { 16 | const fields = getDataFields(); 17 | fields.forEach(({type, els}) => { 18 | const val = cepData[type]; 19 | if (val != null) { 20 | els.forEach((e) => { 21 | if (e instanceof HTMLInputElement) { 22 | e.value = val; 23 | } 24 | }); 25 | } 26 | }); 27 | quickDispatchEvent(CepEvents.CepFieldsAutofilled, {fields, cepData}); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /js/src/handlers/cepFieldsLocker.ts: -------------------------------------------------------------------------------- 1 | import {CepEvents, HandlerParams} from "../types"; 2 | 3 | /* 4 | This handler is responsible for locking the CEP data fields, making them 5 | readonly while the CEP data is fetch. 6 | 7 | So users won't be frustrated if they fill the fields with their own data 8 | and then it's overwritten by the autofill feature. 9 | */ 10 | 11 | export const cepFieldsLockerInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 12 | detail: { 13 | handlerName: "cepFieldsLocker", 14 | installer: cepFieldsLockerInstaller, 15 | }, 16 | }); 17 | 18 | function cepFieldsLockerInstaller({getDataFields, quickAddEventListener}: HandlerParams) { 19 | let lockedFields: Array<{field: HTMLElement; oldValue: string}> = []; 20 | 21 | function restoreFields() { 22 | lockedFields.forEach(({field, oldValue}) => { 23 | if (oldValue === "") { 24 | field.removeAttribute("readonly"); 25 | } else { 26 | field.setAttribute("readonly", oldValue); 27 | } 28 | }); 29 | lockedFields = []; 30 | } 31 | 32 | const removeCepFetchStartListener = quickAddEventListener(CepEvents.CepFetchStart, () => { 33 | const fields = getDataFields(); 34 | 35 | fields.forEach(({type, els}) => { 36 | els.forEach((field) => { 37 | if (formFieldsTags.includes(field.tagName)) { 38 | lockedFields.push({ 39 | field, 40 | oldValue: field.getAttribute("readonly") || "", 41 | }); 42 | field.setAttribute("readonly", "readonly"); 43 | } 44 | }); 45 | }); 46 | }); 47 | 48 | const removeCepFetchErrorListener = quickAddEventListener( 49 | CepEvents.CepFetchError, 50 | restoreFields 51 | ); 52 | const removeCepFieldsAutofilledListener = quickAddEventListener( 53 | CepEvents.CepFieldsAutofilled, 54 | restoreFields 55 | ); 56 | 57 | return () => { 58 | removeCepFetchStartListener(); 59 | removeCepFetchErrorListener(); 60 | removeCepFieldsAutofilledListener(); 61 | }; 62 | } 63 | 64 | const formFieldsTags = ["INPUT", "SELECT", "TEXTAREA"]; 65 | -------------------------------------------------------------------------------- /js/src/handlers/cepLoadingIndicator.ts: -------------------------------------------------------------------------------- 1 | import {CepEvents, HandlerParams} from "../types"; 2 | 3 | export const cepLoadingIndicatorInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 4 | detail: { 5 | handlerName: "cepLoadingIndicator", 6 | installer: cepLoadingIndicatorInstaller, 7 | }, 8 | }); 9 | 10 | export function cepLoadingIndicatorInstaller({quickAddEventListener, fieldData}: HandlerParams) { 11 | const cepField = fieldData.cepField; 12 | const loadingIndicatorId = `${cepField.id}_loading-indicator`; 13 | const loadingIndicator = document.getElementById(loadingIndicatorId); 14 | 15 | if (loadingIndicator != null) { 16 | quickAddEventListener(CepEvents.CepFetchStart, () => { 17 | positionLoadingIndicator(cepField, loadingIndicator); 18 | loadingIndicator.classList.add("visible"); 19 | }); 20 | quickAddEventListener(CepEvents.CepFetchFinish, () => { 21 | loadingIndicator.classList.remove("visible"); 22 | }); 23 | } 24 | } 25 | 26 | function positionLoadingIndicator(cepField: HTMLElement, loadingIndicator: HTMLElement) { 27 | const {style} = loadingIndicator; 28 | const {offsetTop, offsetLeft, offsetWidth, offsetHeight} = cepField; 29 | 30 | style.top = `${offsetTop}px`; 31 | style.left = `${offsetLeft + offsetWidth - loadingIndicator.offsetWidth}px`; 32 | style.height = `${offsetHeight}px`; 33 | } 34 | -------------------------------------------------------------------------------- /js/src/handlers/cepValidator.ts: -------------------------------------------------------------------------------- 1 | import {HandlerParams, CepEvents} from "../types"; 2 | 3 | /* 4 | This handler is responsible for checking if the cleaned value is a valid CEP or not. 5 | 6 | It listen for CepEvents.CEPValueCleaned events and dispatches: 7 | - CepEvents.ValidCepInput 8 | With the cleaned value when it's a valid CEP value 9 | 10 | - CepEvents.InvalidCepInput 11 | With the cleaned value when it's not a valid CEP value 12 | */ 13 | 14 | export const cepValidatorInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 15 | detail: { 16 | handlerName: "cepValidator", 17 | installer: cepValidatorInstaller, 18 | }, 19 | }); 20 | 21 | function cepValidatorInstaller({quickDispatchEvent, quickAddEventListener}: HandlerParams) { 22 | return quickAddEventListener( 23 | CepEvents.CEPValueCleaned, 24 | (cepValue: string, e: CustomEvent): void => { 25 | const cleanedCep = cleanCep((cepValue as unknown) as string); 26 | 27 | if (cleanedCep != null) { 28 | quickDispatchEvent(CepEvents.ValidCepInput, cleanedCep); 29 | } else { 30 | quickDispatchEvent(CepEvents.InvalidCepInput, cepValue); 31 | } 32 | } 33 | ); 34 | } 35 | 36 | function cleanCep(cep: string): string | null { 37 | const match = /^([0-9]{5})[\- ]?([0-9]{3})$/.exec(cep); 38 | return match != null ? match.slice(1, 3).join("") : null; 39 | } 40 | -------------------------------------------------------------------------------- /js/src/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import {cepCleanerInstallEvent} from "./cepCleaner"; 2 | import {cepValidatorInstallEvent} from "./cepValidator"; 3 | import {cepFetcherInstallEvent} from "./cepFetcher"; 4 | import {cepLoadingIndicatorInstallEvent} from "./cepLoadingIndicator"; 5 | import {cepFieldsLockerInstallEvent} from "./cepFieldsLocker"; 6 | import {cepFieldsFillerInstallEvent} from "./cepFieldsFiller"; 7 | import {cepFieldsAutoFocusInstallEvent} from "./cepFieldsAutoFocus"; 8 | 9 | export const defaultInstallerEvents: Array = [ 10 | cepCleanerInstallEvent, 11 | cepValidatorInstallEvent, 12 | cepFetcherInstallEvent, 13 | cepLoadingIndicatorInstallEvent, 14 | cepFieldsLockerInstallEvent, 15 | cepFieldsFillerInstallEvent, 16 | cepFieldsAutoFocusInstallEvent, 17 | ]; 18 | -------------------------------------------------------------------------------- /js/src/index.ts: -------------------------------------------------------------------------------- 1 | import {querySimplecepAutofillFields} from "./fields-finder"; 2 | import {defaultInstallerEvents} from "./handlers"; 3 | import {enableHandlersInstall} from "./install-handlers"; 4 | 5 | /* find all CEP fields in the page and install default defaultHandlers in all of them */ 6 | querySimplecepAutofillFields().map((cepFieldData) => { 7 | enableHandlersInstall(cepFieldData); 8 | defaultInstallerEvents.forEach((event) => cepFieldData.cepField.dispatchEvent(event)); 9 | }); 10 | -------------------------------------------------------------------------------- /js/src/install-handlers.test.ts: -------------------------------------------------------------------------------- 1 | import {HandlerParams} from "./types"; 2 | import {installHandlers} from "./install-handlers"; 3 | 4 | const getById = (id: string): HTMLElement | null => document.getElementById(id); 5 | 6 | describe("installHandlers", () => { 7 | it("should execute each handler once", () => { 8 | const fieldDataMock: any = jest.fn(); 9 | const handler1Mock = jest.fn(); 10 | const handler2Mock = jest.fn(); 11 | 12 | installHandlers(fieldDataMock, [handler1Mock, handler2Mock]); 13 | 14 | expect(handler1Mock.mock.calls.length).toBe(1); 15 | expect(handler2Mock.mock.calls.length).toBe(1); 16 | expect(handler1Mock.mock.calls[0]).toEqual(handler2Mock.mock.calls[0]); 17 | }); 18 | 19 | it("should correctly listen/dispatch DOM node events", () => { 20 | document.body.innerHTML = ''; 21 | const eventData = {my: "data"}; 22 | 23 | const listenerMock = jest.fn(); 24 | 25 | const handler = ({dispatch, addListener}: HandlerParams) => { 26 | addListener("test-event", listenerMock); 27 | dispatch("test-event", eventData); 28 | }; 29 | 30 | installHandlers( 31 | { 32 | cepField: document.getElementById("field") as HTMLInputElement, 33 | baseCepURL: "", 34 | dataFields: [] 35 | }, 36 | [handler] 37 | ); 38 | 39 | expect(listenerMock.mock.calls.length).toBe(1); 40 | expect(listenerMock.mock.calls[0][0]).toBe(eventData); 41 | }); 42 | 43 | it("should generate CEP endpoint URL based on the provided baseCepURL", () => { 44 | installHandlers( 45 | { 46 | cepField: null, 47 | baseCepURL: "/my-custom-cep-endpoint/00000000", 48 | dataFields: [] 49 | }, 50 | [ 51 | ({getCepURL}) => { 52 | expect(getCepURL("12345678")).toBe( 53 | "/my-custom-cep-endpoint/12345678" 54 | ); 55 | } 56 | ] 57 | ); 58 | }); 59 | 60 | it("should create a map of CEP data fields when getDataFields is called", () => { 61 | document.body.innerHTML = ` 62 | 63 |
state
64 | district 65 | `; 66 | 67 | installHandlers( 68 | { 69 | cepField: null, 70 | baseCepURL: "", 71 | dataFields: [ 72 | {type: "state", selector: "#div1"}, 73 | {type: "city", selector: ".city"}, 74 | {type: "district", selector: "#span1"}, 75 | {type: "address", selector: "#non-existent"} 76 | ] 77 | }, 78 | [ 79 | ({getDataFields}) => { 80 | expect(getDataFields()).toEqual([ 81 | {type: "state", els: [getById("div1")]}, 82 | {type: "city", els: [getById("field1")]}, 83 | {type: "district", els: [getById("span1")]}, 84 | {type: "address", els: []} 85 | ]); 86 | } 87 | ] 88 | ); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /js/src/install-handlers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InstallHandlerCustomEvent, 3 | UninstallHandlerCustomEvent, 4 | HandlerParams, 5 | CepEvents, 6 | AutofillFieldDataType, 7 | } from "./types"; 8 | 9 | import {createQuickEventsFuncsFor} from "./quick-events"; 10 | 11 | const createDataFieldsGetter = ( 12 | dataFields: AutofillFieldDataType["dataFields"] 13 | ): HandlerParams["getDataFields"] => () => 14 | dataFields.map(({type, selector}) => ({ 15 | type, 16 | els: Array.prototype.slice 17 | .call(document.querySelectorAll(selector)) 18 | .filter((node: HTMLElement | null) => node != null), 19 | })); 20 | 21 | function getHandlerInstallerParameters(fieldData: AutofillFieldDataType) { 22 | /* create an object with useful data to be sent as param to handler installers */ 23 | const {baseCepURL, dataFields} = fieldData; 24 | const getCepURL: HandlerParams["getCepURL"] = (cep: string): string => 25 | baseCepURL.replace("00000000", cep); 26 | const getDataFields = createDataFieldsGetter(dataFields); 27 | 28 | const {quickAddEventListener, quickDispatchEvent} = createQuickEventsFuncsFor( 29 | fieldData.cepField 30 | ); 31 | 32 | /* when you install a CEP field handler, these are the parameters your 33 | installer function will receive */ 34 | return { 35 | getCepURL, 36 | getDataFields, 37 | fieldData, 38 | quickDispatchEvent, 39 | quickAddEventListener, 40 | }; 41 | } 42 | 43 | export function enableHandlersInstall(fieldData: AutofillFieldDataType) { 44 | // object with all installed handlers as key 45 | // and a func to uninstall them as value 46 | let installedHandlers: {[handlerName: string]: () => undefined} = {}; 47 | const {cepField} = fieldData; 48 | 49 | cepField.addEventListener(CepEvents.InstallHandler, ((event: InstallHandlerCustomEvent) => { 50 | const {installer, handlerName} = event.detail; 51 | 52 | /* it there's already a handler registered with that name, unregister it. 53 | So it's easier for the user to replace any handler */ 54 | if (installedHandlers[handlerName] != null) { 55 | const previousHandlerUninstall = installedHandlers[handlerName]; 56 | previousHandlerUninstall(); 57 | console.log(`Handler '${handlerName}' removed to be replaced.`); 58 | } 59 | 60 | const handlerInstallerParams = getHandlerInstallerParameters(fieldData); 61 | installedHandlers[handlerName] = installer(handlerInstallerParams); 62 | console.log(`Handler '${handlerName}' installed.`); 63 | }) as EventListener); 64 | 65 | cepField.addEventListener(CepEvents.removeHandler, ((event: UninstallHandlerCustomEvent) => { 66 | const {handlerName} = event.detail; 67 | installedHandlers[handlerName](); 68 | console.log(`Handler '${handlerName}' removed.`); 69 | }) as EventListener); 70 | } 71 | -------------------------------------------------------------------------------- /js/src/quick-events.ts: -------------------------------------------------------------------------------- 1 | import {HandlerParams} from "./types"; 2 | 3 | const createDispatcher = (el: HTMLElement): HandlerParams["quickDispatchEvent"] => ( 4 | eventName: string, 5 | detail: any 6 | ) => { 7 | const event = new CustomEvent(eventName, {detail}); 8 | console.log(`Dispatching ${eventName}.`); 9 | el.dispatchEvent(event); 10 | }; 11 | 12 | const createListenerFactory = (el: HTMLElement): HandlerParams["quickAddEventListener"] => ( 13 | eventName: string, 14 | listener: (detail: D, e: CustomEvent) => void 15 | ) => { 16 | const listenerWrapper = (e: CustomEvent) => listener(e.detail, e as CustomEvent); 17 | 18 | el.addEventListener(eventName, listenerWrapper); 19 | console.log(`Event listener registered for '${eventName}'.`); 20 | return () => el.removeEventListener(eventName, listenerWrapper); 21 | }; 22 | 23 | export function createQuickEventsFuncsFor(el: HTMLElement) { 24 | return { 25 | quickAddEventListener: createListenerFactory(el), 26 | quickDispatchEvent: createDispatcher(el), 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /js/src/types.ts: -------------------------------------------------------------------------------- 1 | const dataFieldTypes = ["state", "city", "district", "address"] as const; 2 | 3 | export type DataFieldType = typeof dataFieldTypes[number]; 4 | 5 | export type AutofillFieldDataType = { 6 | cepField: HTMLInputElement; 7 | baseCepURL: string; 8 | dataFields: Array<{type: DataFieldType; selector: string}>; 9 | }; 10 | 11 | export type HandlerParams = { 12 | fieldData: AutofillFieldDataType; 13 | getCepURL: (cep: string) => string; 14 | getDataFields: () => Array<{type: DataFieldType; els: Array}>; 15 | quickDispatchEvent: (eventName: string, detail: any) => void; 16 | quickAddEventListener: ( 17 | eventName: string, 18 | listener: (detail: D, e: CustomEvent) => void 19 | ) => () => void; 20 | }; 21 | 22 | export enum CepEvents { 23 | CEPValueCleaned = "simplecep.CEPValueCleaned", 24 | ValidCepInput = "simplecep.ValidCepInput", 25 | InvalidCepInput = "simplecep.InvalidCepInput", 26 | CepFetchStart = "simplecep.CepFetchStart", 27 | CepFetchSuccess = "simplecep.CepFetchSuccess", 28 | CepFetchError = "simplecep.CepFetchError", 29 | CepFetchFinish = "simplecep.CepFetchFinish", 30 | CepFieldsAutofilled = "simplecep.CepFieldsAutofilled", 31 | 32 | InstallHandler = "simplecep.installHandler", 33 | removeHandler = "simplecep.removeHandler", 34 | } 35 | 36 | export type InstallHandlerCustomEvent = CustomEvent<{ 37 | handlerName: string; 38 | installer: (bla: any) => () => undefined; 39 | }>; 40 | 41 | export type UninstallHandlerCustomEvent = CustomEvent<{handlerName: string}>; 42 | -------------------------------------------------------------------------------- /js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "noImplicitAny": true, 5 | "esModuleInterop": true, 6 | "outDir": "./dist", 7 | "target": "es5", 8 | "moduleResolution": "node" 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_return_any = True 3 | warn_unused_configs = True 4 | ignore_missing_imports = True 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | requirements = ["Django>=2"] 5 | 6 | test_requirements = ["tox==3.14.6", "coverage==5.1"] 7 | 8 | dev_requirements = test_requirements + [ 9 | "mypy==0.770", 10 | "flake8-bugbear==20.1.4", 11 | "black==19.10b0", 12 | ] 13 | 14 | here = os.path.abspath(os.path.dirname(__file__)) 15 | README = open(os.path.join(here, "README.md")).read() 16 | 17 | setup( 18 | name="django-simplecep", 19 | version="0.1.0", 20 | description="Validate brazilian zipcode (CEP) and auto-populate address fields using Correios API data", 21 | long_description=README, 22 | long_description_content_type="text/markdown", 23 | license="MIT", 24 | url="https://github.com/cauethenorio/django-simplecep", 25 | author="Cauê Thenório", 26 | author_email="caue@thenorio.com.br", 27 | packages=["simplecep"], 28 | python_requires=">=3.6", 29 | install_requires=requirements, 30 | extras_require={"dev": dev_requirements, "test": test_requirements}, 31 | keywords=["django", "cep", "correios", "brasil", "endereço"], 32 | classifiers=[ 33 | "Operating System :: POSIX :: Linux", 34 | "Programming Language :: Python :: 3.6", 35 | "Programming Language :: Python :: 3.7", 36 | "Programming Language :: Python :: 3.8", 37 | "Intended Audience :: Developers", 38 | "Framework :: Django", 39 | "Environment :: Web Environment", 40 | "License :: OSI Approved :: MIT License", 41 | "Intended Audience :: Developers", 42 | "Natural Language :: English", 43 | ], 44 | zip_safe=False, 45 | ) 46 | 47 | # https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files 48 | -------------------------------------------------------------------------------- /simplecep/__init__.py: -------------------------------------------------------------------------------- 1 | from simplecep.core import CEPAddress # noqa 2 | from simplecep.providers import NoAvailableCepProviders # noqa 3 | from simplecep.fetcher import get_cep_data # noqa 4 | from simplecep.fields import CEPField # noqa 5 | -------------------------------------------------------------------------------- /simplecep/cache/__init__.py: -------------------------------------------------------------------------------- 1 | from .db_cache import CepDatabaseCache # noqa 2 | -------------------------------------------------------------------------------- /simplecep/cache/db_cache.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from typing import Optional, Iterator 3 | 4 | from simplecep import CEPAddress 5 | from simplecep.models import CepCache 6 | 7 | 8 | class CepDatabaseCache(collections.MutableMapping): 9 | """ 10 | Dict-like class to read and store CEPs to database acting as CEP cache 11 | """ 12 | 13 | def __getitem__(self, cep: str) -> Optional[CEPAddress]: 14 | try: 15 | return CepCache.valid_ceps.get(cep=cep).to_cep_address() 16 | except CepCache.DoesNotExist: 17 | raise KeyError 18 | 19 | def __setitem__(self, cep: str, cepaddress: CEPAddress) -> None: 20 | assert cep == cepaddress.cep, "Key should be the same as capaddress.cep" 21 | CepCache.update_from_cep_address(cepaddress) 22 | 23 | def __delitem__(self, cep: str) -> None: 24 | try: 25 | cep_model = CepCache.valid_ceps.get(cep=cep) 26 | except CepCache.DoesNotExist: 27 | raise KeyError 28 | cep_model.delete() 29 | 30 | def __iter__(self) -> Iterator[CEPAddress]: 31 | for cep_model in CepCache.valid_ceps.order_by("cep"): 32 | yield cep_model.to_cep_address() 33 | 34 | def __len__(self) -> int: 35 | return CepCache.valid_ceps.count() 36 | -------------------------------------------------------------------------------- /simplecep/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.conf import settings 4 | 5 | 6 | DEFAULT_SETTINGS = { 7 | "PROVIDERS": ( 8 | "simplecep.providers.CorreiosSIGEPCEPProvider", 9 | "simplecep.providers.RepublicaVirtualCEPProvider", 10 | "simplecep.providers.ViaCEPProvider", 11 | ), 12 | "CACHE": "simplecep.cache.CepDatabaseCache", 13 | "PROVIDERS_TIMEOUT": 2, 14 | "CEP_CACHE_MAXAGE": datetime.timedelta(days=30 * 6), 15 | } 16 | 17 | 18 | def get_merged_settings(): 19 | """ 20 | Merge default settings into default simple-cep settings 21 | """ 22 | merged = DEFAULT_SETTINGS.copy() 23 | merged.update(getattr(settings, "SIMPLECEP", {})) 24 | return merged 25 | 26 | 27 | simplecep_settings = get_merged_settings() 28 | -------------------------------------------------------------------------------- /simplecep/core.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | 3 | 4 | class CEPAddress: 5 | """ 6 | Represents an address fetched from a CEP number 7 | from cache or provider 8 | """ 9 | 10 | cep: str 11 | state: str 12 | district: Optional[str] 13 | street: Optional[str] 14 | city: str 15 | provider: str 16 | 17 | _data_fields = "cep street state district city provider".split(" ") 18 | 19 | def __init__( 20 | self, cep=None, state=None, city=None, district=None, street=None, provider=None 21 | ): 22 | self.cep = cep 23 | self.state = state 24 | self.city = city 25 | self.district = district 26 | self.street = street 27 | self.provider = provider 28 | 29 | def __repr__(self): 30 | return f"" 31 | 32 | def __eq__(self, other: Any): 33 | if not isinstance(other, CEPAddress): 34 | return False 35 | 36 | return all(getattr(self, f) == getattr(other, f) for f in self._data_fields) 37 | 38 | def to_dict(self, with_provider=False): 39 | data_fields = self._data_fields.copy() 40 | if not with_provider: 41 | data_fields.remove("provider") 42 | 43 | return {field: getattr(self, field) for field in data_fields} 44 | -------------------------------------------------------------------------------- /simplecep/fetcher.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict 2 | 3 | from django.utils.module_loading import import_string 4 | 5 | from simplecep import CEPAddress 6 | from simplecep.conf import simplecep_settings 7 | from simplecep.providers import fetch_from_providers 8 | 9 | 10 | def get_installed_cache() -> Dict: 11 | CEPCache = import_string(simplecep_settings["CACHE"]) 12 | return CEPCache() 13 | 14 | 15 | def get_cep_data(cep: str) -> Optional[CEPAddress]: 16 | cep_cache = get_installed_cache() 17 | try: 18 | return cep_cache[cep] 19 | except KeyError: 20 | cep_address = fetch_from_providers(cep) 21 | if cep_address: 22 | cep_cache[cep] = cep_address 23 | return cep_address 24 | -------------------------------------------------------------------------------- /simplecep/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | from django import forms 5 | from django.core.exceptions import ValidationError, ImproperlyConfigured 6 | from django.urls import reverse 7 | from django.urls.exceptions import NoReverseMatch 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | from simplecep import get_cep_data 11 | from simplecep.providers import NoAvailableCepProviders 12 | 13 | 14 | class CepFieldWidget(forms.TextInput): 15 | template_name = "simplecep/widgets/cep.html" 16 | 17 | def __init__(self, attrs=None, show_loading=True): 18 | self.show_loading = show_loading 19 | super().__init__(attrs) 20 | 21 | def get_context(self, name, value, attrs): 22 | context = super().get_context(name, value, attrs) 23 | context["show_loading"] = self.show_loading 24 | return context 25 | 26 | class Media: 27 | js = ("simplecep/simplecep-autofill.js",) 28 | 29 | 30 | class CEPBoundField(forms.BoundField): 31 | valid_field_types = ("state", "city", "district", "street", "street_number") 32 | 33 | @staticmethod 34 | def get_getcep_url(): 35 | try: 36 | return reverse("simplecep:get-cep", kwargs={"cep": "00000000"}) 37 | except NoReverseMatch: 38 | raise ImproperlyConfigured( 39 | "CEPField autofill used but no " 40 | "'simplecep:get-cep' view found. Include simplecep " 41 | "URLconf in your project urls.py" 42 | ) 43 | 44 | def validate_and_get_fields(self): 45 | fields_keys = self.field.autofill_fields.keys() 46 | valid_field_types = self.valid_field_types 47 | 48 | invalid_fields = list(fields_keys - valid_field_types) 49 | if len(invalid_fields): 50 | raise ImproperlyConfigured( 51 | "Invalid CEPField autofill field type(s): {}. " 52 | "Valid types: {}".format(invalid_fields, valid_field_types) 53 | ) 54 | 55 | # send the fields sorted by valid_field_types order 56 | # the order is important to focus on next empty field via js 57 | return [ 58 | (k, self.field.autofill_fields[k]) 59 | for k in sorted(fields_keys, key=lambda k: valid_field_types.index(k)) 60 | ] 61 | 62 | def get_field_id(self, field_name: str) -> str: 63 | try: 64 | bound_field = self.form[field_name] 65 | except KeyError: 66 | raise ImproperlyConfigured( 67 | "CEPField autofill field not found: '{}'. " 68 | "Valid form fields: {}".format( 69 | field_name, list(self.form.fields.keys() - [self.name]) 70 | ) 71 | ) 72 | return "#{}".format( 73 | bound_field.field.widget.attrs.get("id") or bound_field.auto_id 74 | ) 75 | 76 | def build_widget_attrs(self, attrs, widget=None): 77 | attrs = super().build_widget_attrs(attrs, widget) 78 | 79 | if self.field.autofill_fields is not None: 80 | fields = self.validate_and_get_fields() 81 | 82 | if len(fields): 83 | attrs["data-simplecep-autofill"] = json.dumps( 84 | { 85 | "baseCepURL": self.get_getcep_url(), 86 | "dataFields": [ 87 | { 88 | "type": field_type, 89 | "selector": self.get_field_id(field_name), 90 | } 91 | for field_type, field_name in fields 92 | ], 93 | }, 94 | sort_keys=True, 95 | ) 96 | return attrs 97 | 98 | 99 | class CEPField(forms.CharField): 100 | widget = CepFieldWidget 101 | 102 | default_error_messages = { 103 | "invalid_format": _("Invalid CEP format"), 104 | "not_found": _("CEP not found"), 105 | "no_available_providers": _("No available CEP providers at the moment"), 106 | } 107 | 108 | def __init__(self, *args, autofill=None, **kwargs): 109 | super().__init__(*args, **kwargs) 110 | self.autofill_fields = autofill 111 | 112 | def validate_format(self, value: str) -> str: 113 | match = re.match(r"^(\d{5})-?(\d{3})$", value) 114 | if match is None: 115 | raise ValidationError( 116 | self.error_messages["invalid_format"], code="invalid_format" 117 | ) 118 | return "".join(match.groups()) 119 | 120 | def validate_exists(self, cep): 121 | try: 122 | cep = get_cep_data(cep) 123 | if not cep: 124 | raise ValidationError( 125 | self.error_messages["not_found"], code="not_found" 126 | ) 127 | except NoAvailableCepProviders: 128 | raise ValidationError( 129 | self.error_messages["no_available_providers"], 130 | code="no_available_providers", 131 | ) 132 | # we use this data in get-cep view 133 | self.cep_data = cep 134 | 135 | def clean(self, value: str): 136 | value = super().clean(value) 137 | 138 | if value in self.empty_values: 139 | return value 140 | 141 | raw_cep = self.validate_format(value) 142 | self.validate_exists(raw_cep) 143 | return "{}-{}".format(raw_cep[:5], raw_cep[5:]) 144 | 145 | def widget_attrs(self, widget): 146 | attrs = super().widget_attrs(widget) 147 | 148 | # set a max_length if it's not defined 149 | attrs.setdefault("maxlength", 9) 150 | 151 | # show numeric keyboard on mobile phones 152 | # https://css-tricks.com/everything-you-ever-wanted-to-know-about-inputmode/ 153 | attrs["inputmode"] = "decimal" 154 | 155 | return attrs 156 | 157 | def get_bound_field(self, form, field_name): 158 | return CEPBoundField(form, self, field_name) 159 | -------------------------------------------------------------------------------- /simplecep/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cauethenorio/django-simplecep/acab8a99fe3df8c6a2f01909c07fa36b1ea2d922/simplecep/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simplecep/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-simplecep\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-11-20 14:56-0600\n" 11 | "PO-Revision-Date: 2019-11-20 17:49-0300\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: pt_BR\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | "X-Generator: Poedit 2.2.4\n" 20 | 21 | #: fields.py:101 22 | msgid "Invalid CEP format" 23 | msgstr "Formato de CEP inválido" 24 | 25 | #: fields.py:102 26 | msgid "CEP not found" 27 | msgstr "CEP não encontrado" 28 | 29 | #: fields.py:103 30 | msgid "No available CEP providers at the moment" 31 | msgstr "Nenhum provedor de CEP disponível no momento" 32 | 33 | #: models.py:21 34 | msgid "CEP" 35 | msgstr "CEP" 36 | 37 | #: models.py:22 38 | msgid "State" 39 | msgstr "Estado" 40 | 41 | #: models.py:23 42 | msgid "City" 43 | msgstr "Cidade" 44 | 45 | #: models.py:24 46 | msgid "District" 47 | msgstr "Bairro" 48 | 49 | #: models.py:25 50 | msgid "Address" 51 | msgstr "Endereço" 52 | 53 | #: models.py:27 54 | msgid "Provider" 55 | msgstr "Provedor" 56 | 57 | #: models.py:28 58 | msgid "Updated at" 59 | msgstr "Atualizado em" 60 | -------------------------------------------------------------------------------- /simplecep/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.2 on 2019-11-20 20:10 2 | 3 | from django.db import migrations, models 4 | import django.db.models.manager 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="CepCache", 16 | fields=[ 17 | ( 18 | "cep", 19 | models.CharField( 20 | max_length=8, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="CEP", 24 | ), 25 | ), 26 | ("state", models.CharField(max_length=2, verbose_name="State")), 27 | ("city", models.CharField(max_length=128, verbose_name="City")), 28 | ( 29 | "district", 30 | models.CharField( 31 | max_length=128, null=True, verbose_name="District" 32 | ), 33 | ), 34 | ( 35 | "street", 36 | models.CharField(max_length=128, null=True, verbose_name="Address"), 37 | ), 38 | ("provider", models.CharField(max_length=128, verbose_name="Provider")), 39 | ( 40 | "updated_at", 41 | models.DateTimeField( 42 | auto_now=True, db_index=True, verbose_name="Updated at" 43 | ), 44 | ), 45 | ], 46 | managers=[("valid_ceps", django.db.models.manager.Manager()),], 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /simplecep/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cauethenorio/django-simplecep/acab8a99fe3df8c6a2f01909c07fa36b1ea2d922/simplecep/migrations/__init__.py -------------------------------------------------------------------------------- /simplecep/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from simplecep import CEPAddress 6 | from simplecep.conf import simplecep_settings 7 | 8 | 9 | class ValidCepsManager(models.Manager): 10 | def get_queryset(self): 11 | return ( 12 | super() 13 | .get_queryset() 14 | .filter( 15 | updated_at__gte=timezone.now() - simplecep_settings["CEP_CACHE_MAXAGE"] 16 | ) 17 | ) 18 | 19 | 20 | class CepCache(models.Model): 21 | cep = models.CharField(_("CEP"), max_length=8, primary_key=True) 22 | state = models.CharField(_("State"), max_length=2, null=False) 23 | city = models.CharField(_("City"), max_length=128, null=False) 24 | district = models.CharField(_("District"), max_length=128, null=True) 25 | street = models.CharField(_("Address"), max_length=128, null=True) 26 | 27 | provider = models.CharField(_("Provider"), max_length=128) 28 | updated_at = models.DateTimeField(_("Updated at"), auto_now=True, db_index=True) 29 | 30 | valid_ceps = ValidCepsManager() 31 | all_ceps = models.Manager() 32 | 33 | @classmethod 34 | def update_from_cep_address(cls, cep_address: CEPAddress): 35 | cls.all_ceps.update_or_create( 36 | cep=cep_address.cep, defaults=cep_address.to_dict(with_provider=True) 37 | ) 38 | 39 | def to_cep_address(self) -> CEPAddress: 40 | return CEPAddress( 41 | cep=self.cep, 42 | street=self.street, 43 | state=self.state, 44 | district=self.district, 45 | city=self.city, 46 | provider=self.provider, 47 | ) 48 | -------------------------------------------------------------------------------- /simplecep/providers/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import BaseCEPProvider, CepProviderFetchError # noqa 2 | from .default import ( 3 | CorreiosSIGEPCEPProvider, 4 | ViaCEPProvider, 5 | RepublicaVirtualCEPProvider, 6 | ) # noqa 7 | from .get_installed import get_installed_providers # noqa 8 | from .fetcher import fetch_from_providers, NoAvailableCepProviders # noqa 9 | -------------------------------------------------------------------------------- /simplecep/providers/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import re 3 | import socket 4 | from urllib.request import urlopen, Request 5 | from urllib.error import URLError 6 | 7 | from typing import Optional, Dict 8 | 9 | from simplecep import CEPAddress 10 | 11 | 12 | class CepProviderFetchError(Exception): 13 | pass 14 | 15 | 16 | class BaseCEPProvider(metaclass=abc.ABCMeta): 17 | 18 | # all providers should have an identifier */ 19 | provider_id = None 20 | 21 | def __init__(self, timeout: float = None): 22 | self.timeout: float = timeout 23 | 24 | def request( 25 | self, url, method="GET", data=None, response_encoding="utf-8", headers=None 26 | ): 27 | """ 28 | Helper function to perform HTTP requests 29 | """ 30 | req = Request(url, data=data, method=method, headers=headers or {}) 31 | try: 32 | return urlopen(req, timeout=self.timeout).read().decode(response_encoding) 33 | except (URLError, socket.timeout) as error: 34 | raise CepProviderFetchError(error) 35 | 36 | def clean_cep(self, cep: str) -> str: 37 | match = re.match("^(\\d{5})-?(\\d{3})$", cep) 38 | return "".join(match.groups()) 39 | 40 | def clean_street(self, street: Optional[str]) -> Optional[str]: 41 | """ 42 | Remove numbers from street names (i.e. post office agency CEPs) 43 | """ 44 | if street is not None: 45 | match = re.match(r"^([^,]+),?\s(\d+|s/n)$", street) 46 | if match is not None: 47 | return match.groups()[0] 48 | return street 49 | 50 | def clean(self, fields: Dict) -> CEPAddress: 51 | """ 52 | Subclasses should call this function sending the fields dict 53 | """ 54 | fields = self.extract_district(fields) 55 | 56 | return CEPAddress( 57 | cep=self.clean_cep(fields["cep"]), 58 | state=fields["state"], 59 | city=fields["city"], 60 | district=fields.get("district"), 61 | street=self.clean_street(fields.get("street")), 62 | provider=self.provider_id, 63 | ) 64 | 65 | def extract_district(self, original_fields: Dict): 66 | """ 67 | Extract the Brazilian district name from the city name and send it as 68 | district. Example: 'Jaci Paraná (Porto Velho)' for 76840-000 69 | """ 70 | fields = original_fields.copy() 71 | if fields.get("district") is None: 72 | match = re.match(r"^(.+)\s\((.+)\)$", fields["city"]) 73 | if match: 74 | district, city = match.groups() 75 | fields["district"] = district 76 | fields["city"] = city 77 | return fields 78 | 79 | @abc.abstractmethod 80 | def get_cep_data(self, cep: str) -> Optional[CEPAddress]: 81 | """ 82 | Return the CEP data 83 | """ 84 | -------------------------------------------------------------------------------- /simplecep/providers/default/__init__.py: -------------------------------------------------------------------------------- 1 | from .correios_sigep import CorreiosSIGEPCEPProvider # noqa 2 | from .republicavirtual import RepublicaVirtualCEPProvider # noqa 3 | from .viacep import ViaCEPProvider # noqa 4 | -------------------------------------------------------------------------------- /simplecep/providers/default/correios_sigep.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | from xml.etree import cElementTree as ET 3 | 4 | from simplecep import CEPAddress 5 | from simplecep.providers import BaseCEPProvider, CepProviderFetchError 6 | 7 | 8 | class CorreiosSIGEPCEPProvider(BaseCEPProvider): 9 | # all providers should have a name */ 10 | provider_id = "correios_sigep" 11 | 12 | SIGEP_URL = ( 13 | "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente" 14 | ) 15 | 16 | def envelope(self, cep: str) -> bytearray: 17 | return bytearray( 18 | f""" 19 | 20 | 21 | 22 | 23 | {self.clean_cep(cep)} 24 | 25 | 26 | 27 | """.strip(), 28 | "ascii", 29 | ) 30 | 31 | def unenvelope(self, response: str) -> Optional[Dict]: 32 | try: 33 | return_node = ET.fromstring(response).find(".//return") 34 | except ET.ParseError as e: 35 | raise CepProviderFetchError(e) 36 | if return_node is not None: 37 | return {field.tag: field.text for field in return_node} 38 | return None 39 | 40 | def clean(self, fields) -> CEPAddress: 41 | return super().clean( 42 | { 43 | "cep": fields["cep"], 44 | "state": fields["uf"], 45 | "city": fields["cidade"], 46 | "district": fields.get("bairro"), 47 | "street": fields.get("end"), 48 | } 49 | ) 50 | 51 | def is_cep_not_found_error(self, exc): 52 | """ 53 | Check if the 500 response is about a not found CEP. 54 | We don't want throw errors for that. 55 | """ 56 | if getattr(exc, "code", None) != 500: 57 | return False 58 | 59 | error_response = exc.read().decode("latin1") 60 | try: 61 | message = ET.fromstring(error_response).find(".//faultstring") 62 | except ET.ParseError: 63 | return False 64 | return message is not None and message.text in ( 65 | "CEP INVÁLIDO", 66 | "CEP NAO ENCONTRADO", 67 | ) 68 | 69 | def get_cep_data(self, cep: str) -> Optional[CEPAddress]: 70 | try: 71 | response = self.request( 72 | self.SIGEP_URL, 73 | data=self.envelope(cep), 74 | method="POST", 75 | response_encoding="latin1", 76 | ) 77 | except CepProviderFetchError as e: 78 | original_exc = e.args[0] 79 | if self.is_cep_not_found_error(original_exc): 80 | return None 81 | raise 82 | 83 | fields = self.unenvelope(response) 84 | if fields is not None: 85 | return self.clean(fields) 86 | -------------------------------------------------------------------------------- /simplecep/providers/default/republicavirtual.py: -------------------------------------------------------------------------------- 1 | from json import loads, JSONDecodeError 2 | from typing import Optional 3 | 4 | from simplecep import CEPAddress 5 | from simplecep.providers import BaseCEPProvider, CepProviderFetchError 6 | 7 | 8 | class RepublicaVirtualCEPProvider(BaseCEPProvider): 9 | # all providers should have an identifier */ 10 | provider_id = "republicavirtual" 11 | 12 | def get_api_url(self, cep: str) -> str: 13 | return f"http://cep.republicavirtual.com.br/web_cep.php?cep={self.clean_cep(cep)}&formato=json" 14 | 15 | def get_cep_data(self, cep: str) -> Optional[CEPAddress]: 16 | try: 17 | raw_fields = loads( 18 | self.request( 19 | self.get_api_url(cep), headers={"Accept": "application/json"} 20 | ) 21 | ) 22 | except JSONDecodeError as e: 23 | raise CepProviderFetchError(e) 24 | 25 | if int(raw_fields["resultado"]) > 0: 26 | return self.clean_and_add_cep(raw_fields, cep) 27 | return None 28 | 29 | def clean_state(self, state: str) -> str: 30 | """ 31 | Republica Virtual API returns a different state value when searching 32 | for a district address. (i.e. "RO - Distrito" for 76840-000). 33 | So let's clean it! 34 | """ 35 | return state.split(" ")[0].strip() 36 | 37 | def clean_and_add_cep(self, raw_fields, cep: str) -> CEPAddress: 38 | # remove empty string fields 39 | fields = { 40 | k: value.strip() 41 | for k, value in raw_fields.items() 42 | if value is not None and value.strip() 43 | } 44 | 45 | if fields.get("logradouro") and fields.get("tipo_logradouro"): 46 | fields["street"] = f"{fields['tipo_logradouro']} {fields['logradouro']}" 47 | 48 | return self.clean( 49 | { 50 | "cep": cep, 51 | "state": self.clean_state(fields["uf"]), 52 | "city": fields["cidade"], 53 | "district": fields.get("bairro"), 54 | "street": fields.get("street"), 55 | } 56 | ) 57 | -------------------------------------------------------------------------------- /simplecep/providers/default/viacep.py: -------------------------------------------------------------------------------- 1 | from json import loads, JSONDecodeError 2 | from typing import Optional 3 | 4 | from simplecep import CEPAddress 5 | from simplecep.providers import BaseCEPProvider, CepProviderFetchError 6 | 7 | 8 | class ViaCEPProvider(BaseCEPProvider): 9 | # all providers should have an identifier */ 10 | provider_id = "viacep" 11 | 12 | def get_api_url(self, cep: str) -> str: 13 | return f"https://viacep.com.br/ws/{self.clean_cep(cep)}/json/unicode/" 14 | 15 | def get_cep_data(self, cep: str) -> Optional[CEPAddress]: 16 | try: 17 | raw_fields = loads(self.request(self.get_api_url(cep))) 18 | except JSONDecodeError as e: 19 | raise CepProviderFetchError(e) 20 | 21 | if raw_fields.get("erro") is not True: 22 | return self.clean(raw_fields) 23 | return None 24 | 25 | def clean(self, raw_fields) -> CEPAddress: 26 | # remove empty string fields 27 | fields = {k: value for k, value in raw_fields.items() if value} 28 | 29 | return super().clean( 30 | { 31 | "cep": fields["cep"], 32 | "state": fields["uf"], 33 | "city": fields["localidade"], 34 | "district": fields.get("bairro"), 35 | "street": fields.get("logradouro"), 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /simplecep/providers/fetcher.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from simplecep import CEPAddress 4 | from simplecep.providers import CepProviderFetchError, get_installed_providers 5 | 6 | 7 | providers = get_installed_providers() 8 | 9 | 10 | class NoAvailableCepProviders(Exception): 11 | pass 12 | 13 | 14 | def fetch_from_providers(cep: str) -> Optional[CEPAddress]: 15 | for provider in providers: 16 | try: 17 | return provider.get_cep_data(cep) 18 | except CepProviderFetchError: 19 | pass 20 | raise NoAvailableCepProviders("No CEP Provider available at this time") 21 | -------------------------------------------------------------------------------- /simplecep/providers/get_installed.py: -------------------------------------------------------------------------------- 1 | from typing import List, Type 2 | 3 | from django.utils.module_loading import import_string 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | from simplecep.conf import simplecep_settings 7 | from simplecep.providers.base import BaseCEPProvider 8 | 9 | 10 | def get_installed_providers(): 11 | providers_ids = set() 12 | providers: List[BaseCEPProvider] = [] 13 | 14 | for provider_path in simplecep_settings["PROVIDERS"]: 15 | CEPProvider: Type[BaseCEPProvider] = import_string(provider_path) 16 | provider = CEPProvider(simplecep_settings["PROVIDERS_TIMEOUT"]) 17 | 18 | if not hasattr(provider, "provider_id"): 19 | raise ImproperlyConfigured( 20 | "The {} CEP provider is missing the provider_id propery".format( 21 | provider_path 22 | ) 23 | ) 24 | if provider.provider_id in providers_ids: 25 | raise ImproperlyConfigured( 26 | "More than one provider was created using the same provider_id: {}".format( 27 | provider.provider_id 28 | ) 29 | ) 30 | providers.append(provider) 31 | providers_ids.add(provider.provider_id) 32 | 33 | return providers 34 | -------------------------------------------------------------------------------- /simplecep/static/simplecep/simplecep-autofill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | function querySimplecepAutofillFields() { 5 | var selector = "[data-simplecep-autofill]"; 6 | var fields = []; 7 | document.querySelectorAll(selector).forEach(function (cepField) { 8 | try { 9 | var autoFill = JSON.parse(cepField.dataset.simplecepAutofill); 10 | // delete the attr to avoid adding the same handler multiple times 11 | // when there are more than one form on the page 12 | delete cepField.dataset.simplecepAutofill; 13 | var baseCepURL = autoFill.baseCepURL, dataFields = autoFill.dataFields; 14 | fields.push({ cepField: cepField, baseCepURL: baseCepURL, dataFields: dataFields }); 15 | } 16 | catch (_a) { } 17 | }); 18 | return fields; 19 | } 20 | 21 | var CepEvents; 22 | (function (CepEvents) { 23 | CepEvents["CEPValueCleaned"] = "simplecep.CEPValueCleaned"; 24 | CepEvents["ValidCepInput"] = "simplecep.ValidCepInput"; 25 | CepEvents["InvalidCepInput"] = "simplecep.InvalidCepInput"; 26 | CepEvents["CepFetchStart"] = "simplecep.CepFetchStart"; 27 | CepEvents["CepFetchSuccess"] = "simplecep.CepFetchSuccess"; 28 | CepEvents["CepFetchError"] = "simplecep.CepFetchError"; 29 | CepEvents["CepFetchFinish"] = "simplecep.CepFetchFinish"; 30 | CepEvents["CepFieldsAutofilled"] = "simplecep.CepFieldsAutofilled"; 31 | CepEvents["InstallHandler"] = "simplecep.installHandler"; 32 | CepEvents["removeHandler"] = "simplecep.removeHandler"; 33 | })(CepEvents || (CepEvents = {})); 34 | 35 | /* 36 | This handler is responsible for cleaning the values user inputs on the CEP field. 37 | 38 | It listens for 'input' event on the CEP input field and dispatches: 39 | - CepEvents.CEPValueCleaned 40 | With the cleaned value after the user changes the input value 41 | */ 42 | var cepCleanerInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 43 | detail: { 44 | handlerName: "cepCleaner", 45 | installer: cepMaskInstaller, 46 | }, 47 | }); 48 | function cepMaskInstaller(_a) { 49 | var fieldData = _a.fieldData, quickDispatchEvent = _a.quickDispatchEvent, quickAddEventListener = _a.quickAddEventListener; 50 | return quickAddEventListener("input", function (_, e) { 51 | if (e.target instanceof HTMLInputElement) { 52 | var value = e.target.value; 53 | var _a = format(e.target), formatted = _a[0], start = _a[1], end = _a[2]; 54 | var selectionDelta = 0; 55 | if (formatted.length > 5) { 56 | formatted = formatted.substr(0, 5) + "-" + formatted.substr(5, 3); 57 | if (start > 5) { 58 | selectionDelta += 1; 59 | } 60 | } 61 | e.target.value = formatted; 62 | e.target.selectionStart = Math.max(start + selectionDelta, 0); 63 | e.target.selectionEnd = Math.max(end + selectionDelta, 0); 64 | quickDispatchEvent(CepEvents.CEPValueCleaned, formatted); 65 | } 66 | }); 67 | } 68 | var clean = function (value) { return value.replace(/\D/g, ""); }; 69 | var format = function (el) { 70 | var _a = [el.selectionStart, el.selectionEnd].map(function (i) { 71 | var cleaned = clean(el.value.slice(0, i)); 72 | return i + (cleaned.length - i); 73 | }), start = _a[0], end = _a[1]; 74 | return [clean(el.value), start, end]; 75 | }; 76 | 77 | /* 78 | This handler is responsible for checking if the cleaned value is a valid CEP or not. 79 | 80 | It listen for CepEvents.CEPValueCleaned events and dispatches: 81 | - CepEvents.ValidCepInput 82 | With the cleaned value when it's a valid CEP value 83 | 84 | - CepEvents.InvalidCepInput 85 | With the cleaned value when it's not a valid CEP value 86 | */ 87 | var cepValidatorInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 88 | detail: { 89 | handlerName: "cepValidator", 90 | installer: cepValidatorInstaller, 91 | }, 92 | }); 93 | function cepValidatorInstaller(_a) { 94 | var quickDispatchEvent = _a.quickDispatchEvent, quickAddEventListener = _a.quickAddEventListener; 95 | return quickAddEventListener(CepEvents.CEPValueCleaned, function (cepValue, e) { 96 | var cleanedCep = cleanCep(cepValue); 97 | if (cleanedCep != null) { 98 | quickDispatchEvent(CepEvents.ValidCepInput, cleanedCep); 99 | } 100 | else { 101 | quickDispatchEvent(CepEvents.InvalidCepInput, cepValue); 102 | } 103 | }); 104 | } 105 | function cleanCep(cep) { 106 | var match = /^([0-9]{5})[\- ]?([0-9]{3})$/.exec(cep); 107 | return match != null ? match.slice(1, 3).join("") : null; 108 | } 109 | 110 | /* 111 | This handler is responsible for fetching CEP data when a valid CEP is input. 112 | 113 | It listens for CepEvents.ValidCepInput events and dispatches: 114 | - CepEvents.CepFetchStart 115 | With the cepURL when it request for CEP data 116 | 117 | - CepEvents.CepFetchSuccess 118 | With fetched CEP data on success 119 | 120 | - CepEvents.CepFetchError 121 | With the error when it fails fetching the data 122 | 123 | - CepEvents.CepFetchFinish 124 | With CEP data or error, when the request is finished (with error or not). 125 | */ 126 | var cepFetcherInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 127 | detail: { 128 | handlerName: "cepFetcher", 129 | installer: cepFetcherInstaller, 130 | }, 131 | }); 132 | function cepFetcherInstaller(_a) { 133 | var quickDispatchEvent = _a.quickDispatchEvent, quickAddEventListener = _a.quickAddEventListener, getCepURL = _a.getCepURL; 134 | return quickAddEventListener(CepEvents.ValidCepInput, function (cep) { 135 | var cepURL = getCepURL(cep); 136 | quickDispatchEvent(CepEvents.CepFetchStart, cepURL); 137 | fetchCepData(cepURL) 138 | .then(function (response) { return quickDispatchEvent(CepEvents.CepFetchSuccess, response); }, function (error) { return quickDispatchEvent(CepEvents.CepFetchError, error); }) 139 | .then(function (value) { return quickDispatchEvent(CepEvents.CepFetchFinish, value); }); 140 | }); 141 | } 142 | var fetchCepData = function (url) { 143 | return fetch(url).then(function (response) { 144 | if (response.status >= 200 && response.status < 300) { 145 | return response.json(); 146 | } 147 | else { 148 | var error = new Error(response.statusText || response.status.toString()); 149 | error.response = response; 150 | return Promise.reject(error); 151 | } 152 | }); 153 | }; 154 | 155 | var cepLoadingIndicatorInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 156 | detail: { 157 | handlerName: "cepLoadingIndicator", 158 | installer: cepLoadingIndicatorInstaller, 159 | }, 160 | }); 161 | function cepLoadingIndicatorInstaller(_a) { 162 | var quickAddEventListener = _a.quickAddEventListener, fieldData = _a.fieldData; 163 | var cepField = fieldData.cepField; 164 | var loadingIndicatorId = cepField.id + "_loading-indicator"; 165 | var loadingIndicator = document.getElementById(loadingIndicatorId); 166 | if (loadingIndicator != null) { 167 | quickAddEventListener(CepEvents.CepFetchStart, function () { 168 | positionLoadingIndicator(cepField, loadingIndicator); 169 | loadingIndicator.classList.add("visible"); 170 | }); 171 | quickAddEventListener(CepEvents.CepFetchFinish, function () { 172 | loadingIndicator.classList.remove("visible"); 173 | }); 174 | } 175 | } 176 | function positionLoadingIndicator(cepField, loadingIndicator) { 177 | var style = loadingIndicator.style; 178 | var offsetTop = cepField.offsetTop, offsetLeft = cepField.offsetLeft, offsetWidth = cepField.offsetWidth, offsetHeight = cepField.offsetHeight; 179 | style.top = offsetTop + "px"; 180 | style.left = offsetLeft + offsetWidth - loadingIndicator.offsetWidth + "px"; 181 | style.height = offsetHeight + "px"; 182 | } 183 | 184 | /* 185 | This handler is responsible for locking the CEP data fields, making them 186 | readonly while the CEP data is fetch. 187 | 188 | So users won't be frustrated if they fill the fields with their own data 189 | and then it's overwritten by the autofill feature. 190 | */ 191 | var cepFieldsLockerInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 192 | detail: { 193 | handlerName: "cepFieldsLocker", 194 | installer: cepFieldsLockerInstaller, 195 | }, 196 | }); 197 | function cepFieldsLockerInstaller(_a) { 198 | var getDataFields = _a.getDataFields, quickAddEventListener = _a.quickAddEventListener; 199 | var lockedFields = []; 200 | function restoreFields() { 201 | lockedFields.forEach(function (_a) { 202 | var field = _a.field, oldValue = _a.oldValue; 203 | if (oldValue === "") { 204 | field.removeAttribute("readonly"); 205 | } 206 | else { 207 | field.setAttribute("readonly", oldValue); 208 | } 209 | }); 210 | lockedFields = []; 211 | } 212 | var removeCepFetchStartListener = quickAddEventListener(CepEvents.CepFetchStart, function () { 213 | var fields = getDataFields(); 214 | fields.forEach(function (_a) { 215 | var type = _a.type, els = _a.els; 216 | els.forEach(function (field) { 217 | if (formFieldsTags.includes(field.tagName)) { 218 | lockedFields.push({ 219 | field: field, 220 | oldValue: field.getAttribute("readonly") || "", 221 | }); 222 | field.setAttribute("readonly", "readonly"); 223 | } 224 | }); 225 | }); 226 | }); 227 | var removeCepFetchErrorListener = quickAddEventListener(CepEvents.CepFetchError, restoreFields); 228 | var removeCepFieldsAutofilledListener = quickAddEventListener(CepEvents.CepFieldsAutofilled, restoreFields); 229 | return function () { 230 | removeCepFetchStartListener(); 231 | removeCepFetchErrorListener(); 232 | removeCepFieldsAutofilledListener(); 233 | }; 234 | } 235 | var formFieldsTags = ["INPUT", "SELECT", "TEXTAREA"]; 236 | 237 | var cepFieldsFillerInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 238 | detail: { 239 | handlerName: "cepFieldsFiller", 240 | installer: cepFieldsFillerInstaller, 241 | }, 242 | }); 243 | function cepFieldsFillerInstaller(_a) { 244 | var getDataFields = _a.getDataFields, quickAddEventListener = _a.quickAddEventListener, quickDispatchEvent = _a.quickDispatchEvent; 245 | return quickAddEventListener(CepEvents.CepFetchSuccess, function (cepData) { 246 | var fields = getDataFields(); 247 | fields.forEach(function (_a) { 248 | var type = _a.type, els = _a.els; 249 | var val = cepData[type]; 250 | if (val != null) { 251 | els.forEach(function (e) { 252 | if (e instanceof HTMLInputElement) { 253 | e.value = val; 254 | } 255 | }); 256 | } 257 | }); 258 | quickDispatchEvent(CepEvents.CepFieldsAutofilled, { fields: fields, cepData: cepData }); 259 | }); 260 | } 261 | 262 | var cepFieldsAutoFocusInstallEvent = new CustomEvent(CepEvents.InstallHandler, { 263 | detail: { 264 | handlerName: "focus-next", 265 | installer: cepFieldsAutoFocusInstaller, 266 | }, 267 | }); 268 | function cepFieldsAutoFocusInstaller(_a) { 269 | var quickAddEventListener = _a.quickAddEventListener; 270 | return quickAddEventListener(CepEvents.CepFieldsAutofilled, function (_a) { 271 | var fields = _a.fields, cepData = _a.cepData; 272 | for (var _i = 0, fields_1 = fields; _i < fields_1.length; _i++) { 273 | var _b = fields_1[_i], type = _b.type, els = _b.els; 274 | // search for the first field which returned with no data 275 | if (cepData[type] == null) { 276 | for (var _c = 0, els_1 = els; _c < els_1.length; _c++) { 277 | var el = els_1[_c]; 278 | // search for the first element which is a form field 279 | // attached to the field type 280 | if (formFieldsTags$1.indexOf(el.tagName) >= 0) { 281 | el.focus(); 282 | return; 283 | } 284 | } 285 | } 286 | } 287 | }); 288 | } 289 | var formFieldsTags$1 = ["INPUT", "SELECT", "TEXTAREA"]; 290 | 291 | var defaultInstallerEvents = [ 292 | cepCleanerInstallEvent, 293 | cepValidatorInstallEvent, 294 | cepFetcherInstallEvent, 295 | cepLoadingIndicatorInstallEvent, 296 | cepFieldsLockerInstallEvent, 297 | cepFieldsFillerInstallEvent, 298 | cepFieldsAutoFocusInstallEvent, 299 | ]; 300 | 301 | var createDispatcher = function (el) { return function (eventName, detail) { 302 | var event = new CustomEvent(eventName, { detail: detail }); 303 | console.log("Dispatching " + eventName + "."); 304 | el.dispatchEvent(event); 305 | }; }; 306 | var createListenerFactory = function (el) { return function (eventName, listener) { 307 | var listenerWrapper = function (e) { return listener(e.detail, e); }; 308 | el.addEventListener(eventName, listenerWrapper); 309 | console.log("Event listener registered for '" + eventName + "'."); 310 | return function () { return el.removeEventListener(eventName, listenerWrapper); }; 311 | }; }; 312 | function createQuickEventsFuncsFor(el) { 313 | return { 314 | quickAddEventListener: createListenerFactory(el), 315 | quickDispatchEvent: createDispatcher(el), 316 | }; 317 | } 318 | 319 | var createDataFieldsGetter = function (dataFields) { return function () { 320 | return dataFields.map(function (_a) { 321 | var type = _a.type, selector = _a.selector; 322 | return ({ 323 | type: type, 324 | els: Array.prototype.slice 325 | .call(document.querySelectorAll(selector)) 326 | .filter(function (node) { return node != null; }), 327 | }); 328 | }); 329 | }; }; 330 | function getHandlerInstallerParameters(fieldData) { 331 | /* create an object with useful data to be sent as param to handler installers */ 332 | var baseCepURL = fieldData.baseCepURL, dataFields = fieldData.dataFields; 333 | var getCepURL = function (cep) { 334 | return baseCepURL.replace("00000000", cep); 335 | }; 336 | var getDataFields = createDataFieldsGetter(dataFields); 337 | var _a = createQuickEventsFuncsFor(fieldData.cepField), quickAddEventListener = _a.quickAddEventListener, quickDispatchEvent = _a.quickDispatchEvent; 338 | /* when you install a CEP field handler, these are the parameters your 339 | installer function will receive */ 340 | return { 341 | getCepURL: getCepURL, 342 | getDataFields: getDataFields, 343 | fieldData: fieldData, 344 | quickDispatchEvent: quickDispatchEvent, 345 | quickAddEventListener: quickAddEventListener, 346 | }; 347 | } 348 | function enableHandlersInstall(fieldData) { 349 | // object with all installed handlers as key 350 | // and a func to uninstall them as value 351 | var installedHandlers = {}; 352 | var cepField = fieldData.cepField; 353 | cepField.addEventListener(CepEvents.InstallHandler, (function (event) { 354 | var _a = event.detail, installer = _a.installer, handlerName = _a.handlerName; 355 | /* it there's already a handler registered with that name, unregister it. 356 | So it's easier for the user to replace any handler */ 357 | if (installedHandlers[handlerName] != null) { 358 | var previousHandlerUninstall = installedHandlers[handlerName]; 359 | previousHandlerUninstall(); 360 | console.log("Handler '" + handlerName + "' removed to be replaced."); 361 | } 362 | var handlerInstallerParams = getHandlerInstallerParameters(fieldData); 363 | installedHandlers[handlerName] = installer(handlerInstallerParams); 364 | console.log("Handler '" + handlerName + "' installed."); 365 | })); 366 | cepField.addEventListener(CepEvents.removeHandler, (function (event) { 367 | var handlerName = event.detail.handlerName; 368 | installedHandlers[handlerName](); 369 | console.log("Handler '" + handlerName + "' removed."); 370 | })); 371 | } 372 | 373 | /* find all CEP fields in the page and install default defaultHandlers in all of them */ 374 | querySimplecepAutofillFields().map(function (cepFieldData) { 375 | enableHandlersInstall(cepFieldData); 376 | defaultInstallerEvents.forEach(function (event) { return cepFieldData.cepField.dispatchEvent(event); }); 377 | }); 378 | 379 | }()); 380 | -------------------------------------------------------------------------------- /simplecep/templates/simplecep/widgets/cep-loading-indicator.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /simplecep/templates/simplecep/widgets/cep.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/input.html" %} 2 | {% if show_loading %} 3 | {% include "simplecep/widgets/cep-loading-indicator.html" %} 4 | {% endif %} 5 | -------------------------------------------------------------------------------- /simplecep/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from simplecep.views import CEPView 4 | 5 | 6 | app_name = "simplecep" 7 | 8 | urlpatterns = [re_path("(?P[0-9]{8})/$", CEPView.as_view(), name="get-cep")] 9 | -------------------------------------------------------------------------------- /simplecep/views.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.http import JsonResponse 3 | from django.views import View 4 | 5 | 6 | from simplecep import CEPField 7 | 8 | 9 | class CEPView(View): 10 | errors_statuses = { 11 | "invalid_format": 400, 12 | "not_found": 404, 13 | "no_available_providers": 500, 14 | } 15 | 16 | def get(self, request, *args, cep=None): 17 | cep_field = CEPField() 18 | try: 19 | cep_field.clean(cep) 20 | except ValidationError as e: 21 | return JsonResponse( 22 | {"error": e.code, "message": e.message}, 23 | status=self.errors_statuses[e.code], 24 | ) 25 | 26 | cep_data = cep_field.cep_data 27 | return JsonResponse(cep_data.to_dict()) 28 | -------------------------------------------------------------------------------- /test-manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 6 | from django.core.management import execute_from_command_line 7 | 8 | args = sys.argv + ["simplecep"] 9 | execute_from_command_line(args) 10 | -------------------------------------------------------------------------------- /tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = simplecep 4 | 5 | [report] 6 | ignore_errors = True 7 | omit = 8 | */tests/* 9 | 10 | [html] 11 | directory = coverage_html 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cauethenorio/django-simplecep/acab8a99fe3df8c6a2f01909c07fa36b1ea2d922/tests/__init__.py -------------------------------------------------------------------------------- /tests/empty_urls.py: -------------------------------------------------------------------------------- 1 | urlpatterns = [] 2 | -------------------------------------------------------------------------------- /tests/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cauethenorio/django-simplecep/acab8a99fe3df8c6a2f01909c07fa36b1ea2d922/tests/providers/__init__.py -------------------------------------------------------------------------------- /tests/providers/capture_real_responses.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import socket 4 | import sys 5 | import time 6 | from timeit import default_timer as timer 7 | from unittest import mock 8 | from urllib.request import urlopen 9 | from urllib.error import URLError, HTTPError 10 | 11 | from black import format_str, FileMode 12 | 13 | import django 14 | from django.conf import settings 15 | 16 | from .providers_tests_data import providers_tests_data 17 | 18 | 19 | captured_responses = [] 20 | 21 | 22 | def get_logger(): 23 | """ 24 | Create logger to show to user whats being fetched 25 | """ 26 | logger = logging.getLogger(__name__) 27 | logger.setLevel(logging.INFO) 28 | f_format = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") 29 | c_handler = logging.StreamHandler() 30 | c_handler.setFormatter(f_format) 31 | logger.addHandler(c_handler) 32 | return logger 33 | 34 | 35 | logger = get_logger() 36 | 37 | 38 | class UnexpectedNetworkError(Exception): 39 | """ 40 | On real network errors like timeouts or DNS problems we need to raise 41 | a different errors because URLError is captured inside the providers code. 42 | """ 43 | 44 | 45 | def patched_urlopen(req, timeout): 46 | """ 47 | Captures request and response data and store 48 | """ 49 | captured = { 50 | "request": { 51 | "full_url": req.full_url, 52 | "method": req.method, 53 | "headers": req.headers, 54 | "data": req.data, 55 | } 56 | } 57 | start_timer = timer() 58 | 59 | try: 60 | res = urlopen(req, timeout=timeout) 61 | except HTTPError as error: 62 | captured["response"] = { 63 | "type": "error", 64 | "status": error.status, 65 | "data": error.read(), 66 | } 67 | except (URLError, socket.timeout): 68 | elapsed_time = timer() - start_timer 69 | logger.exception( 70 | "Couldn't capture response (waited {elapsed_time:2.2f}s) from {method:<4} to {full_url}".format( 71 | elapsed_time=elapsed_time, **captured["request"] 72 | ) 73 | ) 74 | sys.exit(1) 75 | else: 76 | captured["response"] = {"type": "success", "data": res.read()} 77 | 78 | elapsed_time = timer() - start_timer 79 | logger.info( 80 | "Captured {response_type:<7} response in {elapsed_time:2.2f}s from " 81 | "{method:<4} to {full_url}".format( 82 | elapsed_time=elapsed_time, 83 | response_type=captured["response"]["type"], 84 | **captured["request"], 85 | ) 86 | ) 87 | 88 | captured_responses.append(captured) 89 | return urlopen(req, timeout=timeout) 90 | 91 | 92 | def save_to_file(data): 93 | filename = "captured_responses.py" 94 | dir_path = os.path.dirname(os.path.realpath(__file__)) 95 | path = os.path.join(dir_path, filename) 96 | 97 | formatted_str = format_str(repr(data), mode=FileMode()) 98 | now_ts = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) 99 | 100 | content = f"""\ 101 | # This file was generated by the "{os.path.basename(__file__)}" script. 102 | # On {now_ts}. 103 | # 104 | # To update it run: 105 | # python -m tests.providers.capture_real_responses 106 | 107 | captured_responses = {formatted_str} 108 | """ 109 | with open(path, "w") as writer: 110 | writer.write(content) 111 | 112 | 113 | def main(): 114 | settings.configure(SIMPLECEP={"PROVIDERS_TIMEOUT": 4}) 115 | django.setup() 116 | 117 | from simplecep.providers.fetcher import providers 118 | 119 | with mock.patch("simplecep.providers.base.urlopen", side_effect=patched_urlopen): 120 | for test_data in providers_tests_data: 121 | for provider in providers: 122 | provider.get_cep_data(test_data["input"]) 123 | save_to_file(captured_responses) 124 | 125 | 126 | if __name__ == "__main__": 127 | main() 128 | -------------------------------------------------------------------------------- /tests/providers/captured_responses.py: -------------------------------------------------------------------------------- 1 | # This file was generated by the "capture_real_responses.py" script. 2 | # On Wed, 20 Nov 2019 19:34:18 +0000. 3 | # 4 | # To update it run: 5 | # python -m tests.providers.capture_real_responses 6 | 7 | captured_responses = [ 8 | { 9 | "request": { 10 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 11 | "method": "POST", 12 | "headers": {}, 13 | "data": bytearray( 14 | b'\n \n \n \n 01001000\n \n \n ' 15 | ), 16 | }, 17 | "response": { 18 | "type": "success", 19 | "data": b'S\xe901001000S\xe3o Paulo- lado \xedmparPra\xe7a da S\xe9SP', 20 | }, 21 | }, 22 | { 23 | "request": { 24 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=01001000&formato=json", 25 | "method": "GET", 26 | "headers": {"Accept": "application/json"}, 27 | "data": None, 28 | }, 29 | "response": { 30 | "type": "success", 31 | "data": b'{"resultado":"1","resultado_txt":"sucesso - cep completo","uf":"SP","cidade":"S\\u00e3o Paulo","bairro":"S\\u00e9","tipo_logradouro":"Pra\\u00e7a","logradouro":"da S\\u00e9"}', 32 | }, 33 | }, 34 | { 35 | "request": { 36 | "full_url": "https://viacep.com.br/ws/01001000/json/unicode/", 37 | "method": "GET", 38 | "headers": {}, 39 | "data": None, 40 | }, 41 | "response": { 42 | "type": "success", 43 | "data": b'{\n "cep": "01001-000",\n "logradouro": "Pra\\u00e7a da S\\u00e9",\n "complemento": "lado \\u00edmpar",\n "bairro": "S\\u00e9",\n "localidade": "S\\u00e3o Paulo",\n "uf": "SP",\n "unidade": "",\n "ibge": "3550308",\n "gia": "1004"\n}', 44 | }, 45 | }, 46 | { 47 | "request": { 48 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 49 | "method": "POST", 50 | "headers": {}, 51 | "data": bytearray( 52 | b'\n \n \n \n 57010240\n \n \n ' 53 | ), 54 | }, 55 | "response": { 56 | "type": "success", 57 | "data": b'Prado57010240Macei\xf3Rua Desembargador Inoc\xeancio LinsAL', 58 | }, 59 | }, 60 | { 61 | "request": { 62 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=57010240&formato=json", 63 | "method": "GET", 64 | "headers": {"Accept": "application/json"}, 65 | "data": None, 66 | }, 67 | "response": { 68 | "type": "success", 69 | "data": b'{"resultado":"1","resultado_txt":"sucesso - cep completo","uf":"AL","cidade":"Macei\\u00f3","bairro":"Prado","tipo_logradouro":"Rua","logradouro":"Desembargador Inoc\\u00eancio Lins"}', 70 | }, 71 | }, 72 | { 73 | "request": { 74 | "full_url": "https://viacep.com.br/ws/57010240/json/unicode/", 75 | "method": "GET", 76 | "headers": {}, 77 | "data": None, 78 | }, 79 | "response": { 80 | "type": "success", 81 | "data": b'{\n "cep": "57010-240",\n "logradouro": "Rua Desembargador Inoc\\u00eancio Lins",\n "complemento": "",\n "bairro": "Prado",\n "localidade": "Macei\\u00f3",\n "uf": "AL",\n "unidade": "",\n "ibge": "2704302",\n "gia": ""\n}', 82 | }, 83 | }, 84 | { 85 | "request": { 86 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 87 | "method": "POST", 88 | "headers": {}, 89 | "data": bytearray( 90 | b'\n \n \n \n 18170000\n \n \n ' 91 | ), 92 | }, 93 | "response": { 94 | "type": "success", 95 | "data": b'18170000PiedadeSP', 96 | }, 97 | }, 98 | { 99 | "request": { 100 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=18170000&formato=json", 101 | "method": "GET", 102 | "headers": {"Accept": "application/json"}, 103 | "data": None, 104 | }, 105 | "response": { 106 | "type": "success", 107 | "data": b'{"resultado":"2","resultado_txt":"sucesso - cep \\u00fanico","uf":"SP","cidade":"Piedade","bairro":"","tipo_logradouro":"","logradouro":"","debug":" - encontrado via search_db cep unico - "}', 108 | }, 109 | }, 110 | { 111 | "request": { 112 | "full_url": "https://viacep.com.br/ws/18170000/json/unicode/", 113 | "method": "GET", 114 | "headers": {}, 115 | "data": None, 116 | }, 117 | "response": { 118 | "type": "success", 119 | "data": b'{\n "cep": "18170-000",\n "logradouro": "",\n "complemento": "",\n "bairro": "",\n "localidade": "Piedade",\n "uf": "SP",\n "unidade": "",\n "ibge": "3537800",\n "gia": "5265"\n}', 120 | }, 121 | }, 122 | { 123 | "request": { 124 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 125 | "method": "POST", 126 | "headers": {}, 127 | "data": bytearray( 128 | b'\n \n \n \n 78175000\n \n \n ' 129 | ), 130 | }, 131 | "response": { 132 | "type": "success", 133 | "data": b'78175000Pocon\xe9MT', 134 | }, 135 | }, 136 | { 137 | "request": { 138 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=78175000&formato=json", 139 | "method": "GET", 140 | "headers": {"Accept": "application/json"}, 141 | "data": None, 142 | }, 143 | "response": { 144 | "type": "success", 145 | "data": b'{"resultado":"2","resultado_txt":"sucesso - cep \\u00fanico","uf":"MT","cidade":"Pocon\\u00e9","bairro":"","tipo_logradouro":"","logradouro":"","debug":" - encontrado via search_db cep unico - "}', 146 | }, 147 | }, 148 | { 149 | "request": { 150 | "full_url": "https://viacep.com.br/ws/78175000/json/unicode/", 151 | "method": "GET", 152 | "headers": {}, 153 | "data": None, 154 | }, 155 | "response": { 156 | "type": "success", 157 | "data": b'{\n "cep": "78175-000",\n "logradouro": "",\n "complemento": "",\n "bairro": "",\n "localidade": "Pocon\\u00e9",\n "uf": "MT",\n "unidade": "",\n "ibge": "5106505",\n "gia": ""\n}', 158 | }, 159 | }, 160 | { 161 | "request": { 162 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 163 | "method": "POST", 164 | "headers": {}, 165 | "data": bytearray( 166 | b'\n \n \n \n 63200970\n \n \n ' 167 | ), 168 | }, 169 | "response": { 170 | "type": "success", 171 | "data": b'Centro63200970Miss\xe3o VelhaRua Jos\xe9 Sobreira da Cruz 271CE', 172 | }, 173 | }, 174 | { 175 | "request": { 176 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=63200970&formato=json", 177 | "method": "GET", 178 | "headers": {"Accept": "application/json"}, 179 | "data": None, 180 | }, 181 | "response": { 182 | "type": "success", 183 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"1","resultado_txt":"sucesso - cep completo","uf":"CE","cidade":"Miss\\u00e3o Velha","bairro":"Centro\\u00a0","tipo_logradouro":"Rua","logradouro":"Jos\\u00e9 Sobreira da Cruz, 271 "}', 184 | }, 185 | }, 186 | { 187 | "request": { 188 | "full_url": "https://viacep.com.br/ws/63200970/json/unicode/", 189 | "method": "GET", 190 | "headers": {}, 191 | "data": None, 192 | }, 193 | "response": { 194 | "type": "success", 195 | "data": b'{\n "cep": "63200-970",\n "logradouro": "Rua Jos\\u00e9 Sobreira da Cruz 271",\n "complemento": "",\n "bairro": "Centro",\n "localidade": "Miss\\u00e3o Velha",\n "uf": "CE",\n "unidade": "",\n "ibge": "2308401",\n "gia": ""\n}', 196 | }, 197 | }, 198 | { 199 | "request": { 200 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 201 | "method": "POST", 202 | "headers": {}, 203 | "data": bytearray( 204 | b'\n \n \n \n 69096970\n \n \n ' 205 | ), 206 | }, 207 | "response": { 208 | "type": "success", 209 | "data": b'Cidade Nova69096970ManausAvenida Noel Nutels 1350AM', 210 | }, 211 | }, 212 | { 213 | "request": { 214 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=69096970&formato=json", 215 | "method": "GET", 216 | "headers": {"Accept": "application/json"}, 217 | "data": None, 218 | }, 219 | "response": { 220 | "type": "success", 221 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"1","resultado_txt":"sucesso - cep completo","uf":"AM","cidade":"Manaus","bairro":"Cidade Nova\\u00a0","tipo_logradouro":"Avenida","logradouro":"Noel Nutels, 1350 "}', 222 | }, 223 | }, 224 | { 225 | "request": { 226 | "full_url": "https://viacep.com.br/ws/69096970/json/unicode/", 227 | "method": "GET", 228 | "headers": {}, 229 | "data": None, 230 | }, 231 | "response": { 232 | "type": "success", 233 | "data": b'{\n "cep": "69096-970",\n "logradouro": "Avenida Noel Nutels 1350",\n "complemento": "",\n "bairro": "Cidade Nova",\n "localidade": "Manaus",\n "uf": "AM",\n "unidade": "",\n "ibge": "1302603",\n "gia": ""\n}', 234 | }, 235 | }, 236 | { 237 | "request": { 238 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 239 | "method": "POST", 240 | "headers": {}, 241 | "data": bytearray( 242 | b'\n \n \n \n 20010974\n \n \n ' 243 | ), 244 | }, 245 | "response": { 246 | "type": "success", 247 | "data": b'Centro20010974Rio de JaneiroRua Primeiro de Mar\xe7o 64RJ', 248 | }, 249 | }, 250 | { 251 | "request": { 252 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=20010974&formato=json", 253 | "method": "GET", 254 | "headers": {"Accept": "application/json"}, 255 | "data": None, 256 | }, 257 | "response": { 258 | "type": "success", 259 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"1","resultado_txt":"sucesso - cep completo","uf":"RJ","cidade":"Rio de Janeiro","bairro":"Centro\\u00a0","tipo_logradouro":"Rua","logradouro":"Primeiro de Mar\\u00e7o, 64 "}', 260 | }, 261 | }, 262 | { 263 | "request": { 264 | "full_url": "https://viacep.com.br/ws/20010974/json/unicode/", 265 | "method": "GET", 266 | "headers": {}, 267 | "data": None, 268 | }, 269 | "response": { 270 | "type": "success", 271 | "data": b'{\n "cep": "20010-974",\n "logradouro": "Rua Primeiro de Mar\\u00e7o",\n "complemento": "64",\n "bairro": "Centro",\n "localidade": "Rio de Janeiro",\n "uf": "RJ",\n "unidade": "",\n "ibge": "3304557",\n "gia": ""\n}', 272 | }, 273 | }, 274 | { 275 | "request": { 276 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 277 | "method": "POST", 278 | "headers": {}, 279 | "data": bytearray( 280 | b'\n \n \n \n 96010900\n \n \n ' 281 | ), 282 | }, 283 | "response": { 284 | "type": "success", 285 | "data": b'Centro96010900PelotasRua Tiradentes 2515RS', 286 | }, 287 | }, 288 | { 289 | "request": { 290 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=96010900&formato=json", 291 | "method": "GET", 292 | "headers": {"Accept": "application/json"}, 293 | "data": None, 294 | }, 295 | "response": { 296 | "type": "success", 297 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"1","resultado_txt":"sucesso - cep completo","uf":"RS","cidade":"Pelotas","bairro":"Centro\\u00a0","tipo_logradouro":"Rua","logradouro":"Tiradentes, 2515 "}', 298 | }, 299 | }, 300 | { 301 | "request": { 302 | "full_url": "https://viacep.com.br/ws/96010900/json/unicode/", 303 | "method": "GET", 304 | "headers": {}, 305 | "data": None, 306 | }, 307 | "response": { 308 | "type": "success", 309 | "data": b'{\n "cep": "96010-900",\n "logradouro": "Rua Tiradentes",\n "complemento": "2515",\n "bairro": "Centro",\n "localidade": "Pelotas",\n "uf": "RS",\n "unidade": "",\n "ibge": "4314407",\n "gia": ""\n}', 310 | }, 311 | }, 312 | { 313 | "request": { 314 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 315 | "method": "POST", 316 | "headers": {}, 317 | "data": bytearray( 318 | b'\n \n \n \n 38101990\n \n \n ' 319 | ), 320 | }, 321 | "response": { 322 | "type": "success", 323 | "data": b'38101990Baixa (Uberaba)Rua Bas\xedlio Eug\xeanio dos SantosMG', 324 | }, 325 | }, 326 | { 327 | "request": { 328 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=38101990&formato=json", 329 | "method": "GET", 330 | "headers": {"Accept": "application/json"}, 331 | "data": None, 332 | }, 333 | "response": { 334 | "type": "success", 335 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"1","resultado_txt":"sucesso - cep completo","uf":"MG - Distrito","cidade":"Baixa (Uberaba)","bairro":"\\u00a0","tipo_logradouro":"Rua","logradouro":"Bas\\u00edlio Eug\\u00eanio dos Santos "}', 336 | }, 337 | }, 338 | { 339 | "request": { 340 | "full_url": "https://viacep.com.br/ws/38101990/json/unicode/", 341 | "method": "GET", 342 | "headers": {}, 343 | "data": None, 344 | }, 345 | "response": { 346 | "type": "success", 347 | "data": b'{\n "cep": "38101-990",\n "logradouro": "Rua Bas\\u00edlio Eug\\u00eanio dos Santos",\n "complemento": "",\n "bairro": "Baixa",\n "localidade": "Uberaba",\n "uf": "MG",\n "unidade": "",\n "ibge": "3170107",\n "gia": ""\n}', 348 | }, 349 | }, 350 | { 351 | "request": { 352 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 353 | "method": "POST", 354 | "headers": {}, 355 | "data": bytearray( 356 | b'\n \n \n \n 76840000\n \n \n ' 357 | ), 358 | }, 359 | "response": { 360 | "type": "success", 361 | "data": b'76840000Jaci Paran\xe1 (Porto Velho)RO', 362 | }, 363 | }, 364 | { 365 | "request": { 366 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=76840000&formato=json", 367 | "method": "GET", 368 | "headers": {"Accept": "application/json"}, 369 | "data": None, 370 | }, 371 | "response": { 372 | "type": "success", 373 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"2","resultado_txt":"sucesso - cep \\u00fanico","uf":"RO - Distrito","cidade":"Jaci Paran\\u00e1 (Porto Velho)","bairro":"\\u00a0","tipo_logradouro":"","logradouro":""}', 374 | }, 375 | }, 376 | { 377 | "request": { 378 | "full_url": "https://viacep.com.br/ws/76840000/json/unicode/", 379 | "method": "GET", 380 | "headers": {}, 381 | "data": None, 382 | }, 383 | "response": { 384 | "type": "success", 385 | "data": b'{\n "cep": "76840-000",\n "logradouro": "",\n "complemento": "",\n "bairro": "",\n "localidade": "Jaci Paran\\u00e1 (Porto Velho)",\n "uf": "RO",\n "unidade": "",\n "ibge": "1100205",\n "gia": ""\n}', 386 | }, 387 | }, 388 | { 389 | "request": { 390 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 391 | "method": "POST", 392 | "headers": {}, 393 | "data": bytearray( 394 | b'\n \n \n \n 86055991\n \n \n ' 395 | ), 396 | }, 397 | "response": { 398 | "type": "success", 399 | "data": b'86055991LondrinaRodovia M\xe1bio Gon\xe7alves Palhano, s/nPR', 400 | }, 401 | }, 402 | { 403 | "request": { 404 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=86055991&formato=json", 405 | "method": "GET", 406 | "headers": {"Accept": "application/json"}, 407 | "data": None, 408 | }, 409 | "response": { 410 | "type": "success", 411 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"1","resultado_txt":"sucesso - cep completo","uf":"PR","cidade":"Londrina","bairro":"\\u00a0","tipo_logradouro":"Rodovia","logradouro":"M\\u00e1bio Gon\\u00e7alves Palhano, s\\/n "}', 412 | }, 413 | }, 414 | { 415 | "request": { 416 | "full_url": "https://viacep.com.br/ws/86055991/json/unicode/", 417 | "method": "GET", 418 | "headers": {}, 419 | "data": None, 420 | }, 421 | "response": { 422 | "type": "success", 423 | "data": b'{\n "cep": "86055-991",\n "logradouro": "Rodovia M\\u00e1bio Gon\\u00e7alves Palhano",\n "complemento": "s/n",\n "bairro": "",\n "localidade": "Londrina",\n "uf": "PR",\n "unidade": "",\n "ibge": "4113700",\n "gia": ""\n}', 424 | }, 425 | }, 426 | { 427 | "request": { 428 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 429 | "method": "POST", 430 | "headers": {}, 431 | "data": bytearray( 432 | b'\n \n \n \n 00000000\n \n \n ' 433 | ), 434 | }, 435 | "response": { 436 | "type": "error", 437 | "status": 500, 438 | "data": b'soap:ServerCEP INV\xc1LIDOCEP INV\xc1LIDO', 439 | }, 440 | }, 441 | { 442 | "request": { 443 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=00000000&formato=json", 444 | "method": "GET", 445 | "headers": {"Accept": "application/json"}, 446 | "data": None, 447 | }, 448 | "response": { 449 | "type": "success", 450 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"0","resultado_txt":"sucesso - cep n\\u00e3o encontrado","uf":"","cidade":"","bairro":"","tipo_logradouro":"","logradouro":""}', 451 | }, 452 | }, 453 | { 454 | "request": { 455 | "full_url": "https://viacep.com.br/ws/00000000/json/unicode/", 456 | "method": "GET", 457 | "headers": {}, 458 | "data": None, 459 | }, 460 | "response": {"type": "success", "data": b'{\n "erro": true\n}'}, 461 | }, 462 | { 463 | "request": { 464 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 465 | "method": "POST", 466 | "headers": {}, 467 | "data": bytearray( 468 | b'\n \n \n \n 11111111\n \n \n ' 469 | ), 470 | }, 471 | "response": { 472 | "type": "error", 473 | "status": 500, 474 | "data": b'soap:ServerCEP INV\xc1LIDOCEP INV\xc1LIDO', 475 | }, 476 | }, 477 | { 478 | "request": { 479 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=11111111&formato=json", 480 | "method": "GET", 481 | "headers": {"Accept": "application/json"}, 482 | "data": None, 483 | }, 484 | "response": { 485 | "type": "success", 486 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"0","resultado_txt":"sucesso - cep n\\u00e3o encontrado","uf":"","cidade":"","bairro":"","tipo_logradouro":"","logradouro":""}', 487 | }, 488 | }, 489 | { 490 | "request": { 491 | "full_url": "https://viacep.com.br/ws/11111111/json/unicode/", 492 | "method": "GET", 493 | "headers": {}, 494 | "data": None, 495 | }, 496 | "response": {"type": "success", "data": b'{\n "erro": true\n}'}, 497 | }, 498 | { 499 | "request": { 500 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 501 | "method": "POST", 502 | "headers": {}, 503 | "data": bytearray( 504 | b'\n \n \n \n 99999999\n \n \n ' 505 | ), 506 | }, 507 | "response": { 508 | "type": "error", 509 | "status": 500, 510 | "data": b'soap:ServerCEP INV\xc1LIDOCEP INV\xc1LIDO', 511 | }, 512 | }, 513 | { 514 | "request": { 515 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=99999999&formato=json", 516 | "method": "GET", 517 | "headers": {"Accept": "application/json"}, 518 | "data": None, 519 | }, 520 | "response": { 521 | "type": "success", 522 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"0","resultado_txt":"sucesso - cep n\\u00e3o encontrado","uf":"","cidade":"","bairro":"","tipo_logradouro":"","logradouro":""}', 523 | }, 524 | }, 525 | { 526 | "request": { 527 | "full_url": "https://viacep.com.br/ws/99999999/json/unicode/", 528 | "method": "GET", 529 | "headers": {}, 530 | "data": None, 531 | }, 532 | "response": {"type": "success", "data": b'{\n "erro": true\n}'}, 533 | }, 534 | { 535 | "request": { 536 | "full_url": "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente", 537 | "method": "POST", 538 | "headers": {}, 539 | "data": bytearray( 540 | b'\n \n \n \n 01111110\n \n \n ' 541 | ), 542 | }, 543 | "response": { 544 | "type": "success", 545 | "data": b'', 546 | }, 547 | }, 548 | { 549 | "request": { 550 | "full_url": "http://cep.republicavirtual.com.br/web_cep.php?cep=01111110&formato=json", 551 | "method": "GET", 552 | "headers": {"Accept": "application/json"}, 553 | "data": None, 554 | }, 555 | "response": { 556 | "type": "success", 557 | "data": b'{"debug":" - nao encontrado via search_db cep unico - ","resultado":"0","resultado_txt":"sucesso - cep n\\u00e3o encontrado","uf":"","cidade":"","bairro":"","tipo_logradouro":"","logradouro":""}', 558 | }, 559 | }, 560 | { 561 | "request": { 562 | "full_url": "https://viacep.com.br/ws/01111110/json/unicode/", 563 | "method": "GET", 564 | "headers": {}, 565 | "data": None, 566 | }, 567 | "response": {"type": "success", "data": b'{\n "erro": true\n}'}, 568 | }, 569 | ] 570 | 571 | -------------------------------------------------------------------------------- /tests/providers/providers_tests_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file data is used in both files: 3 | - capture_real_responses.py 4 | - test_providers_data.py 5 | 6 | The "capture_real_responses.py" script sends this data input keys values to 7 | installed providers and store the responses in the "captured_responses.py" file. 8 | 9 | The "test_providers_data.py" test case read this data as tests inputs, 10 | uses the "captured_responses.py" file data to simulate the providers responses 11 | and compare the providers outputs with the "expected_result" keys values. 12 | 13 | If you change this data, run "capture_real_responses.py" to update the 14 | "captured_responses.py" file content. 15 | """ 16 | 17 | providers_tests_data = [ 18 | { 19 | "input": "01001000", 20 | "expected_result": { 21 | "cep": "01001000", 22 | "state": "SP", 23 | "city": "São Paulo", 24 | "district": "Sé", 25 | "street": "Praça da Sé", 26 | }, 27 | }, 28 | { 29 | "input": "57010-240", 30 | "expected_result": { 31 | "cep": "57010240", 32 | "state": "AL", 33 | "city": "Maceió", 34 | "district": "Prado", 35 | "street": "Rua Desembargador Inocêncio Lins", 36 | }, 37 | }, 38 | { 39 | "input": "18170000", 40 | "expected_result": { 41 | "cep": "18170000", 42 | "state": "SP", 43 | "city": "Piedade", 44 | "district": None, 45 | "street": None, 46 | }, 47 | }, 48 | { 49 | "input": "78175-000", 50 | "expected_result": { 51 | "cep": "78175000", 52 | "state": "MT", 53 | "city": "Poconé", 54 | "district": None, 55 | "street": None, 56 | }, 57 | }, 58 | { 59 | "input": "63200-970", 60 | "expected_result": { 61 | "cep": "63200970", 62 | "state": "CE", 63 | "city": "Missão Velha", 64 | "district": "Centro", 65 | "street": "Rua José Sobreira da Cruz", 66 | }, 67 | }, 68 | { 69 | "input": "69096-970", 70 | "expected_result": { 71 | "cep": "69096970", 72 | "state": "AM", 73 | "city": "Manaus", 74 | "district": "Cidade Nova", 75 | "street": "Avenida Noel Nutels", 76 | }, 77 | }, 78 | { 79 | "input": "20010-974", 80 | "expected_result": { 81 | "cep": "20010974", 82 | "state": "RJ", 83 | "city": "Rio de Janeiro", 84 | "district": "Centro", 85 | "street": "Rua Primeiro de Março", 86 | }, 87 | }, 88 | { 89 | "input": "96010-900", 90 | "expected_result": { 91 | "cep": "96010900", 92 | "state": "RS", 93 | "city": "Pelotas", 94 | "district": "Centro", 95 | "street": "Rua Tiradentes", 96 | }, 97 | }, 98 | { 99 | "input": "38101990", 100 | "expected_result": { 101 | "cep": "38101990", 102 | "state": "MG", 103 | "city": "Uberaba", 104 | "district": "Baixa", 105 | "street": "Rua Basílio Eugênio dos Santos", 106 | }, 107 | }, 108 | { 109 | "input": "76840-000", 110 | "expected_result": { 111 | "cep": "76840000", 112 | "state": "RO", 113 | "city": "Porto Velho", 114 | "district": "Jaci Paraná", 115 | "street": None, 116 | }, 117 | }, 118 | { 119 | "input": "86055991", 120 | "expected_result": { 121 | "cep": "86055991", 122 | "city": "Londrina", 123 | "district": None, 124 | "state": "PR", 125 | "street": "Rodovia Mábio Gonçalves Palhano", 126 | }, 127 | }, 128 | {"input": "00000000", "expected_result": None}, 129 | {"input": "11111111", "expected_result": None}, 130 | {"input": "99999999", "expected_result": None}, 131 | {"input": "01111110", "expected_result": None}, 132 | ] 133 | -------------------------------------------------------------------------------- /tests/providers/test_providers_expected_responses.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from pprint import pformat 3 | from unittest import mock 4 | from urllib.response import addinfourl 5 | from urllib.error import HTTPError 6 | 7 | from django.test import TestCase 8 | 9 | from simplecep import CEPAddress 10 | from simplecep.providers.fetcher import providers 11 | from .providers_tests_data import providers_tests_data 12 | from .captured_responses import captured_responses 13 | 14 | 15 | def patched_urlopen(req, timeout): 16 | """" 17 | A customized version of urlopen which will take the responses from 18 | the captured_responses.py file instead of triggering real HTTP requests. 19 | """ 20 | req_dict = { 21 | "full_url": req.full_url, 22 | "method": req.method, 23 | "headers": req.headers, 24 | "data": req.data, 25 | } 26 | 27 | for messages in captured_responses: 28 | if messages["request"] == req_dict: 29 | response = messages["response"] 30 | 31 | if response["type"] == "success": 32 | # Create a fake response object with the same data was captured 33 | # from the real endpoint 34 | return addinfourl(BytesIO(response["data"]), {}, req_dict["full_url"]) 35 | elif response["type"] == "error": 36 | # Create a fake response error object with the same data 37 | # captured from the real endpoint 38 | raise HTTPError( 39 | req_dict["full_url"], 40 | response["status"], 41 | "Fake Error", 42 | {}, 43 | BytesIO(response["data"]), 44 | ) 45 | 46 | raise ValueError( 47 | f"No stored response found for:\n {pformat(req_dict)} request.\n\n" 48 | "Please run the script to capture real providers responses with:\n" 49 | "$ python -m tests.providers.capture_real_responses\n\nAnd try again." 50 | ) 51 | 52 | 53 | class ProvidersExpectedResponsesTestCase(TestCase): 54 | def test_expected_providers_responses(self): 55 | # bye real urlopen and welcome our patched version which skips 56 | # real requests and return captured_responses.py file content 57 | with mock.patch( 58 | "simplecep.providers.base.urlopen", side_effect=patched_urlopen 59 | ): 60 | for test_data in providers_tests_data: 61 | cep = test_data["input"] 62 | expected_result = test_data["expected_result"] 63 | 64 | with self.subTest(test_data=test_data): 65 | for provider in providers: 66 | 67 | if expected_result is not None: 68 | expected_cep_address = CEPAddress( 69 | provider=provider.provider_id, **expected_result 70 | ) 71 | else: 72 | expected_cep_address = None 73 | 74 | with self.subTest(provider=provider.__class__.__name__): 75 | cep_address = provider.get_cep_data(cep) 76 | self.assertEqual(cep_address, expected_cep_address) 77 | -------------------------------------------------------------------------------- /tests/providers/test_providers_unexpected_responses.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from unittest import mock 3 | import socket 4 | from urllib.response import addinfourl 5 | from urllib.error import HTTPError, URLError 6 | 7 | from django.test import TestCase 8 | 9 | from simplecep.providers import CepProviderFetchError 10 | from simplecep.providers.fetcher import providers 11 | 12 | 13 | class ProvidersUnexpectedResponsesTestCase(TestCase): 14 | def assert_fetcherror_is_raised_when(self, error=None, response=None): 15 | def fake_urlopen(*args, **kwargs): 16 | if error is not None: 17 | raise error 18 | return response 19 | 20 | with mock.patch("simplecep.providers.base.urlopen", side_effect=fake_urlopen): 21 | for provider in providers: 22 | with self.subTest(provider=provider.__class__.__name__): 23 | with self.assertRaises(CepProviderFetchError): 24 | provider.get_cep_data("12345689") 25 | 26 | def test_providers_timeout_should_raise_proper_error(self): 27 | self.assert_fetcherror_is_raised_when(error=socket.timeout) 28 | 29 | def test_providers_dns_error_should_raise_proper_error(self): 30 | dns_error = URLError( 31 | "gaierror(8, 'nodename nor servname provided, or not known')", 32 | ) 33 | self.assert_fetcherror_is_raised_when(error=dns_error) 34 | 35 | def test_providers_gateway_timeout_should_raise_proper_error(self): 36 | gateway_timeout_error = HTTPError( 37 | "https://fake-url.exampple.com", 38 | 504, 39 | "Fake Gateway Timeout Error", 40 | {}, 41 | BytesIO(b"Gateway Timeout"), 42 | ) 43 | self.assert_fetcherror_is_raised_when(error=gateway_timeout_error) 44 | 45 | def test_fake_success_response_raise_proper_error(self): 46 | fake_success_response = addinfourl( 47 | BytesIO(b"Unexpected Content Here " + bytes(range(256))), 48 | {}, 49 | "https://fake-url.exampple.com", 50 | ) 51 | self.assert_fetcherror_is_raised_when(response=fake_success_response) 52 | 53 | def test_webserver_error_raise_proper_error(self): 54 | webserver_error = HTTPError( 55 | "https://fake-url.exampple.com", 56 | 500, 57 | "Webserver Error", 58 | {}, 59 | BytesIO(b"I'm misconfigured" + bytes(range(256)) + b""), 60 | ) 61 | self.assert_fetcherror_is_raised_when(error=webserver_error) 62 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | INSTALLED_APPS = ("simplecep",) 2 | 3 | ROOT_URLCONF = "tests.urls" 4 | 5 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 6 | 7 | SECRET_KEY = "abracadabra" 8 | -------------------------------------------------------------------------------- /tests/test_cep_db_cache.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.test import TestCase 4 | from django.utils import timezone 5 | 6 | from simplecep.cache import CepDatabaseCache 7 | from simplecep.conf import simplecep_settings 8 | from simplecep.models import CepCache 9 | from simplecep import CEPAddress 10 | 11 | 12 | class CepDatabaseCacheTestCase(TestCase): 13 | def get_sample_cep_address(self, cep="10000001"): 14 | return CEPAddress( 15 | cep=cep, 16 | street="Rua", 17 | state="XX", 18 | district="Centro", 19 | city="Rio Redondo", 20 | provider="fake", 21 | ) 22 | 23 | def create_ceps_in_db(self, num_to_create=1): 24 | created = [] 25 | for n in range(num_to_create): 26 | cep = "{:0>8}".format(n) 27 | cep_address = self.get_sample_cep_address(cep=cep) 28 | CepCache.update_from_cep_address(cep_address) 29 | created.append(cep_address) 30 | return created 31 | 32 | def assert_num_ceps_in_db(self, num: int) -> None: 33 | self.assertEqual(CepCache.valid_ceps.count(), num) 34 | 35 | def test_assigning_should_add_to_cache(self): 36 | db_cache = CepDatabaseCache() 37 | sample_cep_address = self.get_sample_cep_address() 38 | db_cache["10000001"] = sample_cep_address 39 | self.assert_num_ceps_in_db(1) 40 | self.assertEqual( 41 | CepCache.valid_ceps.first().to_cep_address(), sample_cep_address, 42 | ) 43 | 44 | def test_assigning_with_wrong_cep_value_should_raise(self): 45 | db_cache = CepDatabaseCache() 46 | sample_cep_address = self.get_sample_cep_address() 47 | with self.assertRaises(AssertionError): 48 | db_cache["10000002"] = sample_cep_address 49 | 50 | def test_getting_should_read_from_cache(self): 51 | db_cache = CepDatabaseCache() 52 | (cep_address,) = self.create_ceps_in_db(1) 53 | 54 | self.assert_num_ceps_in_db(1) 55 | self.assertEqual( 56 | db_cache[cep_address.cep], CepCache.valid_ceps.first().to_cep_address(), 57 | ) 58 | 59 | def test_getting_inexistent_should_raise_keyerror(self): 60 | db_cache = CepDatabaseCache() 61 | with self.assertRaises(KeyError): 62 | _ = db_cache["10000001"] 63 | 64 | def test_deleting_should_remove_from_cache(self): 65 | db_cache = CepDatabaseCache() 66 | (cep_address,) = self.create_ceps_in_db(1) 67 | self.assert_num_ceps_in_db(1) 68 | del db_cache[cep_address.cep] 69 | self.assert_num_ceps_in_db(0) 70 | with self.assertRaises(KeyError): 71 | del db_cache[cep_address.cep] 72 | 73 | def test_iterating_should_read_all_cache_items(self): 74 | (sample1, sample2) = self.create_ceps_in_db(2) 75 | db_cache = CepDatabaseCache() 76 | ceps_list = [c.cep for c in db_cache] 77 | self.assertListEqual(ceps_list, [sample1.cep, sample2.cep]) 78 | 79 | def test_len_should_count_all_cache_items(self): 80 | db_cache = CepDatabaseCache() 81 | self.create_ceps_in_db(3) 82 | self.assertEqual(len(db_cache), 3) 83 | 84 | def test_stale_cep_should_be_skiped_by_cache(self): 85 | db_cache = CepDatabaseCache() 86 | (sample_cep,) = self.create_ceps_in_db(1) 87 | self.assertIsNotNone(db_cache[sample_cep.cep]) 88 | 89 | timeout_limit = timezone.now() - simplecep_settings["CEP_CACHE_MAXAGE"] 90 | one_sec = timedelta(seconds=1) 91 | 92 | # cep is still valid 93 | CepCache.all_ceps.update(updated_at=timeout_limit - one_sec) 94 | with self.assertRaises(KeyError): 95 | _ = db_cache[sample_cep.cep] 96 | 97 | # cep is no longer valid 98 | CepCache.all_ceps.update(updated_at=timeout_limit + one_sec) 99 | self.assertIsNotNone(db_cache[sample_cep.cep]) 100 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from simplecep import CEPAddress 4 | 5 | 6 | class CEPAddressTestCase(TestCase): 7 | def get_cepaddress_sample(self): 8 | return CEPAddress( 9 | cep="00000111", 10 | street="Rua", 11 | state="XX", 12 | district="Centro", 13 | city="Rio Redondo", 14 | provider="fake", 15 | ) 16 | 17 | def test_equals_should_be_true_only_exact_the_same(self): 18 | cep_address = self.get_cepaddress_sample() 19 | self.assertEqual( 20 | cep_address, CEPAddress(**cep_address.to_dict(with_provider=True)) 21 | ) 22 | self.assertNotEqual(cep_address, CEPAddress(**cep_address.to_dict())) 23 | self.assertNotEqual(cep_address, 1) 24 | self.assertNotEqual(cep_address, "") 25 | 26 | def test_repr_doesnt_raise(self): 27 | cep_address = self.get_cepaddress_sample() 28 | repr(cep_address) 29 | 30 | def test_assert_provider_is_not_in_generated_dict(self): 31 | cep_address = self.get_cepaddress_sample() 32 | self.assertFalse(hasattr(cep_address.to_dict(), "provider")) 33 | -------------------------------------------------------------------------------- /tests/test_fetcher.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | from django.test import TestCase 4 | 5 | from simplecep.fetcher import get_cep_data 6 | 7 | 8 | @patch("simplecep.fetcher.fetch_from_providers") 9 | @patch("simplecep.fetcher.import_string") 10 | class FetcherTestCase(TestCase): 11 | def test_fetcher_should_get_from_cache_when_its_available( 12 | self, import_string_mock, fetch_from_providers_mock 13 | ): 14 | return_mock = Mock() 15 | import_string_mock.return_value.return_value = {"12345678": return_mock} 16 | 17 | self.assertEqual(get_cep_data("12345678"), return_mock) 18 | fetch_from_providers_mock.assert_not_called() 19 | 20 | def test_fetcher_should_get_from_providers_when_not_in_cache( 21 | self, import_string_mock, fetch_from_providers_mock 22 | ): 23 | fake_cache = {} 24 | import_string_mock.return_value.return_value = fake_cache 25 | self.assertEqual( 26 | get_cep_data("12345678"), fetch_from_providers_mock.return_value 27 | ) 28 | import_string_mock.return_value.assert_called_once() 29 | # and save the fetched value into the cache 30 | self.assertEqual(fake_cache["12345678"], fetch_from_providers_mock.return_value) 31 | -------------------------------------------------------------------------------- /tests/test_form.py: -------------------------------------------------------------------------------- 1 | import html 2 | import json 3 | from unittest.mock import patch 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.test import TestCase, override_settings 7 | from django import forms 8 | 9 | from simplecep import CEPField, NoAvailableCepProviders 10 | 11 | 12 | def html_decode(html_fragment) -> str: 13 | return html.escape(json.dumps(html_fragment, sort_keys=True)) 14 | 15 | 16 | @patch("simplecep.fields.get_cep_data", autospec=True) 17 | class CEPFormTestCase(TestCase): 18 | def test_cep_field_format_validation(self, *args): 19 | invalid_format_message = CEPField.default_error_messages["invalid_format"] 20 | 21 | self.assertFieldOutput( 22 | CEPField, 23 | { 24 | "18170000": "18170-000", 25 | "18170-000": "18170-000", 26 | " 01001000": "01001-000", 27 | "01001-000": "01001-000", 28 | }, 29 | { 30 | "10aa122": [invalid_format_message], 31 | "a": [invalid_format_message], 32 | "0000000": [invalid_format_message], 33 | "000000-000": [invalid_format_message], 34 | "18170-0000": [invalid_format_message], 35 | }, 36 | ) 37 | 38 | def test_cep_field_cep_not_found_validation(self, mocked_get_cep_data): 39 | not_found_message = CEPField.default_error_messages["not_found"] 40 | mocked_get_cep_data.return_value = None 41 | self.assertFieldOutput( 42 | CEPField, 43 | {}, 44 | { 45 | "11111111": [not_found_message], 46 | "11111-111": [not_found_message], 47 | "12345-123": [not_found_message], 48 | }, 49 | ) 50 | 51 | def test_cep_field_cep_found_validation(self, mocked_get_cep_data): 52 | # The real function will return a CEPAdress instance 53 | # but the fields only checks if it's null 54 | mocked_get_cep_data.return_value = True 55 | self.assertFieldOutput( 56 | CEPField, 57 | { 58 | "11111111": "11111-111", 59 | "11111-111": "11111-111", 60 | "12345-123": "12345-123", 61 | }, 62 | {}, 63 | ) 64 | 65 | def test_cep_field_no_available_providers_validation(self, mocked_get_cep_data): 66 | no_providers_message = CEPField.default_error_messages["no_available_providers"] 67 | mocked_get_cep_data.side_effect = NoAvailableCepProviders() 68 | self.assertFieldOutput(CEPField, {}, {"11111111": [no_providers_message]}) 69 | 70 | def test_cep_field_maxlength_should_be_9_as_default(self, *args): 71 | class SimpleForm(forms.Form): 72 | cep = CEPField() 73 | 74 | form = SimpleForm() 75 | self.assertIn('maxlength="9"', form["cep"].as_widget()) 76 | 77 | class AnotherSimpleForm(forms.Form): 78 | cep = CEPField(max_length=12) 79 | 80 | form = AnotherSimpleForm() 81 | self.assertIn('maxlength="12"', form["cep"].as_widget()) 82 | 83 | def test_cep_field_should_not_add_autofill_attrs_by_default(self, *args): 84 | class SimpleForm(forms.Form): 85 | cep = CEPField() 86 | 87 | form = SimpleForm() 88 | self.assertNotIn('data-simplecep-autofill"', form["cep"].as_widget()) 89 | 90 | def test_cep_autofill_should_fail_with_wrong_keys(self, *args): 91 | class SimpleForm(forms.Form): 92 | cep = CEPField(autofill={"states": "state"}) 93 | 94 | with self.assertRaisesMessage( 95 | ImproperlyConfigured, 96 | "Invalid CEPField autofill field type(s): ['states']. " 97 | "Valid types: ('state', 'city', 'district', 'street', 'street_number')", 98 | ): 99 | form = SimpleForm() 100 | form["cep"].as_widget() 101 | 102 | def test_cep_autofill_should_fail_with_wrong_field_name(self, *args): 103 | class SimpleForm(forms.Form): 104 | cep = CEPField(autofill={"state": "eztado"}) 105 | estado = forms.CharField() 106 | 107 | with self.assertRaisesMessage( 108 | ImproperlyConfigured, 109 | "CEPField autofill field not found: 'eztado'. " 110 | "Valid form fields: ['estado']", 111 | ): 112 | form = SimpleForm() 113 | form["cep"].as_widget() 114 | 115 | @override_settings(ROOT_URLCONF="tests.empty_urls") 116 | def test_cep_field_autofill_should_fail_without_getcep_endpoint(self, *args): 117 | class SimpleForm(forms.Form): 118 | cep = CEPField(autofill={"state": "state"}) 119 | state = forms.CharField() 120 | 121 | with self.assertRaisesMessage( 122 | ImproperlyConfigured, 123 | "CEPField autofill used but no 'simplecep:get-cep' view found", 124 | ): 125 | form = SimpleForm() 126 | form["cep"].as_widget() 127 | 128 | def test_cep_field_autofill_should_send_baseurl(self, *args): 129 | class SimpleForm(forms.Form): 130 | cep = CEPField(autofill={"city": "cidade"}) 131 | cidade = forms.CharField() 132 | 133 | form = SimpleForm() 134 | field_html = form["cep"].as_widget() 135 | self.assertIn(html.escape('"baseCepURL": "/cep/00000000/"'), field_html) 136 | 137 | def test_cep_field_empty_autofill_should_create_attrs(self, *args): 138 | class SimpleForm(forms.Form): 139 | cep = CEPField(autofill={"district": "bairro_xyz"}) 140 | bairro_xyz = forms.CharField() 141 | 142 | form = SimpleForm() 143 | field_html = form["cep"].as_widget() 144 | 145 | self.assertIn("data-simplecep-autofill=", field_html) 146 | self.assertIn( 147 | html_decode( 148 | [{"type": "district", "selector": "#" + form["bairro_xyz"].auto_id}] 149 | ), 150 | field_html, 151 | ) 152 | 153 | def test_cep_field_should_correctly_use_custom_fields_ids(self, *args): 154 | custom_id = "my_custom_id" 155 | 156 | class SimpleForm(forms.Form): 157 | cep = CEPField(autofill={"street": "endereco"}) 158 | endereco = forms.CharField(widget=forms.TextInput(attrs={"id": custom_id})) 159 | 160 | form = SimpleForm() 161 | field_html = form["cep"].as_widget() 162 | 163 | self.assertIn("data-simplecep-autofill", field_html) 164 | self.assertIn( 165 | html_decode([{"type": "street", "selector": "#" + custom_id}]), field_html 166 | ) 167 | -------------------------------------------------------------------------------- /tests/test_providers.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | from simplecep.providers import ( 7 | BaseCEPProvider, 8 | CepProviderFetchError, 9 | get_installed_providers, 10 | fetch_from_providers, 11 | NoAvailableCepProviders, 12 | ) 13 | 14 | 15 | class SimpleCepSettingsTestCase(TestCase): 16 | @mock.patch("simplecep.providers.get_installed.import_string", autospec=True) 17 | def test_valid_providers_instantiation_should_work(self, mocked_import_string): 18 | timeout_config_mock = mock.Mock() 19 | providers_classes_mock = [ 20 | mock.Mock(spec=BaseCEPProvider), 21 | mock.Mock(spec=BaseCEPProvider), 22 | mock.Mock(spec=BaseCEPProvider), 23 | ] 24 | mocked_import_string.side_effect = providers_classes_mock 25 | 26 | with mock.patch.dict( 27 | "simplecep.providers.get_installed.simplecep_settings", 28 | {"PROVIDERS_TIMEOUT": timeout_config_mock}, 29 | ): 30 | providers = get_installed_providers() 31 | 32 | for provider_class_mock in providers_classes_mock: 33 | provider_class_mock.assert_called_once_with(timeout_config_mock) 34 | 35 | self.assertListEqual( 36 | providers, [m.return_value for m in providers_classes_mock] 37 | ) 38 | 39 | @mock.patch("simplecep.providers.get_installed.import_string", autospec=True) 40 | def test_providers_without_provider_id_should_error_out(self, mocked_import_string): 41 | # mocks a provider class without the provider_id attr 42 | providers_classes_mock = [mock.Mock(return_value=mock.Mock(spec=[]))] 43 | mocked_import_string.side_effect = providers_classes_mock 44 | with self.assertRaises(ImproperlyConfigured): 45 | get_installed_providers() 46 | 47 | @mock.patch("simplecep.providers.get_installed.import_string", autospec=True) 48 | def test_duplicated_provider_ids_should_error_out(self, mocked_import_string): 49 | 50 | providers_classes_mock = [ 51 | mock.Mock(return_value=mock.Mock(provider_id="repeated")), 52 | mock.Mock(return_value=mock.Mock(provider_id="repeated")), 53 | ] 54 | mocked_import_string.side_effect = providers_classes_mock 55 | with self.assertRaises(ImproperlyConfigured): 56 | get_installed_providers() 57 | 58 | def fill_providers_mock_with(self, providers_mock, providers_types): 59 | providers_type_map = { 60 | "valid": mock.Mock(get_cep_data=mock.Mock()), 61 | "unavailable": mock.Mock( 62 | get_cep_data=mock.Mock(side_effect=CepProviderFetchError()) 63 | ), 64 | } 65 | providers_mock.__iter__.return_value = [ 66 | providers_type_map[provider_type] for provider_type in providers_types 67 | ] 68 | return providers_type_map.values() 69 | 70 | @mock.patch("simplecep.providers.fetcher.providers", autospec=True) 71 | def test_all_providers_should_be_tried_until_a_working_one(self, providers_mock): 72 | valid_mock, unavailable_mock, = self.fill_providers_mock_with( 73 | providers_mock, ["unavailable", "unavailable", "valid"] 74 | ) 75 | 76 | self.assertEqual(fetch_from_providers(""), valid_mock.get_cep_data.return_value) 77 | self.assertEqual(unavailable_mock.get_cep_data.call_count, 2) 78 | valid_mock.get_cep_data.assert_called_once() 79 | 80 | @mock.patch("simplecep.providers.fetcher.providers", autospec=True) 81 | def test_subsequent_providers_should_not_be_run_after_a_working_one( 82 | self, providers_mock 83 | ): 84 | valid_mock, unavailable_mock, = self.fill_providers_mock_with( 85 | providers_mock, ["valid", "unavailable", "unavailable"] 86 | ) 87 | self.assertEqual(fetch_from_providers(""), valid_mock.get_cep_data.return_value) 88 | valid_mock.get_cep_data.assert_called_once() 89 | unavailable_mock.assert_not_called() 90 | 91 | @mock.patch("simplecep.providers.fetcher.providers", autospec=True) 92 | def test_call_providers_until_finding_a_working_one(self, providers_mock): 93 | valid_mock, unavailable_mock, = self.fill_providers_mock_with( 94 | providers_mock, ["unavailable", "valid", "unavailable"] 95 | ) 96 | self.assertEqual(fetch_from_providers(""), valid_mock.get_cep_data.return_value) 97 | valid_mock.get_cep_data.assert_called_once() 98 | unavailable_mock.get_cep_data.assert_called_once() 99 | 100 | @mock.patch("simplecep.providers.fetcher.providers", autospec=True) 101 | def test_no_available_providers_should_raise(self, providers_mock): 102 | valid_mock, unavailable_mock, = self.fill_providers_mock_with( 103 | providers_mock, ["unavailable", "unavailable", "unavailable"] 104 | ) 105 | with self.assertRaises(NoAvailableCepProviders): 106 | fetch_from_providers("") 107 | self.assertEqual(unavailable_mock.get_cep_data.call_count, 3) 108 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | 5 | from simplecep.conf import DEFAULT_SETTINGS, get_merged_settings 6 | 7 | 8 | class SimpleCepSettingsTestCase(TestCase): 9 | def test_should_use_default_when_project_has_no_settings(self): 10 | self.assertEqual(DEFAULT_SETTINGS, get_merged_settings()) 11 | 12 | def get_should_merge_when_project_has_partial_settings(self): 13 | PROJECT_CONFIG = {"PROVIDERS_TIMEOUT": mock.Mock(), "PROVIDERS": mock.Mock()} 14 | 15 | with self.settings(SIMPLECEP=PROJECT_CONFIG): 16 | merged = get_merged_settings() 17 | expected = DEFAULT_SETTINGS.copy() 18 | expected.update(PROJECT_CONFIG) 19 | self.assertEqual(merged, expected) 20 | -------------------------------------------------------------------------------- /tests/test_view.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.urls import reverse 4 | from django.test import TestCase 5 | 6 | from .utils import TEST_DATA 7 | from simplecep import CEPField, CEPAddress, NoAvailableCepProviders 8 | 9 | 10 | class ViewTestCase(TestCase): 11 | @patch("simplecep.fields.get_cep_data", autospec=True) 12 | def test_view_existing_cep_should_return_cep_data(self, mocked_get_cep_data): 13 | for cep_data in TEST_DATA: 14 | mocked_get_cep_data.return_value = CEPAddress( 15 | cep=cep_data["cep"], 16 | state=cep_data["state"], 17 | city=cep_data["city"], 18 | district=cep_data["district"], 19 | street=cep_data["street"], 20 | ) 21 | response = self.client.get( 22 | reverse("simplecep:get-cep", kwargs={"cep": cep_data["cep"]}) 23 | ) 24 | self.assertEqual(response.status_code, 200) 25 | self.assertEqual(response.json(), cep_data) 26 | 27 | @patch("simplecep.fields.get_cep_data", autospec=True) 28 | def test_view_inexistent_cep_should_return_404_error(self, mocked_get_cep_data): 29 | mocked_get_cep_data.return_value = None 30 | response = self.client.get( 31 | reverse("simplecep:get-cep", kwargs={"cep": "00000000"}) 32 | ) 33 | self.assertEqual(response.status_code, 404) 34 | self.assertEqual( 35 | response.json(), 36 | { 37 | "error": "not_found", 38 | "message": CEPField.default_error_messages["not_found"], 39 | }, 40 | ) 41 | 42 | @patch("simplecep.fields.get_cep_data", autospec=True) 43 | def test_view_should_return_error_when_no_providers_are_available( 44 | self, mocked_get_cep_data 45 | ): 46 | mocked_get_cep_data.side_effect = NoAvailableCepProviders() 47 | response = self.client.get( 48 | reverse("simplecep:get-cep", kwargs={"cep": "00000000"}) 49 | ) 50 | self.assertEqual(response.status_code, 500) 51 | self.assertEqual( 52 | response.json(), 53 | { 54 | "error": "no_available_providers", 55 | "message": CEPField.default_error_messages["no_available_providers"], 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | urlpatterns = [path("cep/", include("simplecep.urls"))] 4 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from unittest.mock import patch, Mock 3 | from urllib.error import URLError 4 | 5 | 6 | @contextmanager 7 | def mock_urlopen(): 8 | with patch("simplecep.providers.base.urlopen", autospec=True) as mock_urlopen: 9 | mock_response = Mock() 10 | mock_response.read.side_effect = [URLError("Network error")] 11 | mock_urlopen.return_value = mock_response 12 | yield 13 | 14 | 15 | TEST_DATA = [ 16 | { 17 | "street": "Praça da Sé", 18 | "cep": "01001000", 19 | "city": "São Paulo", 20 | "district": "Sé", 21 | "state": "SP", 22 | }, 23 | { 24 | "street": None, 25 | "cep": "18170000", 26 | "city": "Piedade", 27 | "district": None, 28 | "state": "SP", 29 | }, 30 | { 31 | "street": "Avenida Presidente Castelo Branco", 32 | "cep": "62880970", 33 | "city": "Horizonte", 34 | "district": "Centro", 35 | "state": "CE", 36 | }, 37 | { 38 | "street": "Rodovia Mábio Gonçalves Palhano", 39 | "cep": "86055991", 40 | "city": "Londrina", 41 | "district": None, 42 | "state": "PR", 43 | }, 44 | ] 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{36,37,38}-django20 4 | py{36,37,38}-django21 5 | py{36,37,38}-django22 6 | py{36,37,38}-django30 7 | py{36,37,38}-django30 8 | 9 | [testenv] 10 | deps = 11 | django20: Django>=2.0,<2.1 12 | django21: Django>=2.1,<2.2 13 | django22: Django>=2.2,<2.3 14 | django30: Django>=3.0,<3.1 15 | commands = 16 | pip install -e .[test] 17 | python manage.py test 18 | --------------------------------------------------------------------------------