├── .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 |
21 |
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 |
--------------------------------------------------------------------------------