Toolbox da Bode.io para projetos em Django — JWT authentication, utility models, helpers and services.
- JWT authentication via
djangorestframework-simplejwt - DRF endpoints: login, logout, register, password change, password reset request/confirm, e-mail confirmation and Google social login
- Abstract base models:
BaseModel(timestamps) andLogicDeletable(soft deletion) - Built-in models:
UserAuth,Pais,LoginRecord,ConsultaCEP,OptimizedImageWithTinyPNG - CEP lookup service with ViaCEP / AwesomeAPI fallback and database caching
- Login tracking via Django signals
- Utility functions: file cleaners, date/string/number formatters, workday calculator, email obfuscation, MX-validating form field, PBKDF2 hashing, pagination fix, MySQL
ROUND - Template tags:
grana(R$ formatting),multiply,roi,url_replace - Sentry metrics helpers (optional)
- Management commands: country import, TinyPNG image compression
- Standardized success and error responses across all endpoints
pip install git+https://github.com/bodedev/bodepontoio.git@matheusOptional extras:
pip install "django-bodepontoio[tinify]" # TinyPNG image compression
pip install "django-bodepontoio[sentry]" # Sentry metrics helpers# settings.py
INSTALLED_APPS = [
...
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist", # required for LogoutView
"bodepontoio",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"EXCEPTION_HANDLER": "bodepontoio.exceptions.exception_handler",
"DEFAULT_RENDERER_CLASSES": ["bodepontoio.renderers.SuccessJSONRenderer"],
}
SIMPLE_JWT = {
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
}
# Optional bodepontoio overrides
BODEPONTOIO = {
"FRONTEND_URL": "https://app.example.com", # default: "http://localhost:3000"
"PASSWORD_RESET_URL_PATH": "/reset/{uid}/{token}/", # default: "/reset-password/{uid}/{token}/"
"GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com", # required for Google login
}# urls.py
path("api/auth/", include("bodepontoio.urls", namespace="bodepontoio")),python manage.py migrate| Method | URL | Permission | Description |
|---|---|---|---|
| POST | login/ |
Public | Obtain access + refresh tokens |
| POST | token/refresh/ |
Public | Refresh access token |
| POST | logout/ |
Authenticated | Blacklist refresh token |
| POST | register/ |
Public | Create account, sends confirmation email |
| POST | password/change/ |
Authenticated | Change password |
| POST | password/reset/ |
Public | Request reset email |
| POST | password/reset/confirm/ |
Public | Confirm reset with uid + token |
| GET | email/confirm/<uid>/<token>/ |
Public | Confirm email address |
| POST | email/confirm/resend/ |
Public | Re-send confirmation email |
| POST | social/google/ |
Public | Login or register via Google ID token |
POST login/ accepts:
| Field | Required | Notes |
|---|---|---|
login |
Yes | Username or email address |
password |
Yes |
POST register/ accepts:
| Field | Required | Notes |
|---|---|---|
username |
Yes | Must be unique |
email |
Yes | Must be unique |
password |
Yes | Minimum 8 characters |
first_name |
No | |
last_name |
No |
A confirmation email is sent automatically. The user cannot log in until the email is confirmed.
POST social/google/ accepts {"id_token": "<google-oauth2-id-token>"}.
Pass the ID token issued by Google's OAuth 2.0 flow (e.g. from Google Sign-In for Web or a mobile SDK). The endpoint verifies the token against the configured client ID, then:
- Creates a new user (with email, first_name, last_name from the token) if one doesn't exist yet
- Marks the email as verified automatically
- Returns the same
access+refreshtokens as regular login
Requires BODEPONTOIO["GOOGLE_CLIENT_ID"] to be set.
When a user registers, a confirmation email is sent automatically. Login is blocked until the email address is confirmed.
- User registers → confirmation email sent
- User clicks the link in the email →
GET email/confirm/<uid>/<token>/→ account activated - User can now log in
POST email/confirm/resend/ accepts {"email": "..."}. It silently does nothing if the address is unknown or already confirmed (anti-enumeration).
Django template override (no config needed): create a file at the same path inside any directory listed in TEMPLATES[0]['DIRS']:
your_project/
templates/
bodepontoio/
email_confirmation_email.html
| Variable | Description |
|---|---|
user |
The User instance |
confirm_url |
Full confirmation URL |
A styled HTML email (with plain-text fallback) is sent automatically when a user requests a password reset.
Django template override (no config needed): create a file at the same path inside any directory listed in TEMPLATES[0]['DIRS']:
your_project/
templates/
bodepontoio/
password_reset_email.html
| Variable | Description |
|---|---|
user |
The User instance requesting the reset |
reset_url |
Full URL the user should click to reset their password |
All responses share a consistent envelope.
Success:
{
"success": true,
"pagination": false,
"data": { ... }
}Success — empty body (e.g. logout):
{
"success": true
}Success — paginated list (any pagination class using BodePaginationMixin):
{
"success": true,
"pagination": true,
"data": { ... }
}The shape of data depends on the pagination class. For StandardPagination:
{
"success": true,
"pagination": true,
"data": {
"items": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
}
}
}Validation error (HTTP 400):
{
"success": false,
"type": "validation_error",
"errors": [
{"field": "email", "message": "Este campo é obrigatório."},
{"field": "non_field_errors", "message": "Credenciais inválidas."}
]
}All other errors:
{
"success": false,
"type": "authentication_error",
"error": "As credenciais de autenticação não foram fornecidas."
}Possible type values:
validation_errorauthentication_errorpermission_errornot_foundparse_errormethod_not_allowednot_acceptableunsupported_media_typethrottled
Unknown exception types fall back to the class name in snake_case.
Mixin that marks paginated responses so SuccessJSONRenderer can wrap them correctly. Any custom pagination class can opt in by inheriting from it:
from bodepontoio.pagination import BodePaginationMixin
from rest_framework.pagination import PageNumberPagination
class MyPagination(BodePaginationMixin, PageNumberPagination):
...A PageNumberPagination subclass (with BodePaginationMixin) with a consistent response shape.
from bodepontoio.pagination import StandardPagination
class MyView(ListAPIView):
pagination_class = StandardPagination| Query param | Default | Max | Description |
|---|---|---|---|
page |
1 |
— | Page number |
limit |
20 |
100 |
Items per page |
from bodepontoio.models import BaseModel, LogicDeletable
class Invoice(LogicDeletable):
...Adds created and updated (both DateTimeField, auto-managed) to any model.
Extends BaseModel with soft deletion. Fields added: excluido (bool), excluido_em (datetime), excluido_por (FK to auth.User, nullable).
# Querying
Invoice.objects.all() # live rows only (default manager)
Invoice.com_excluidos.all() # includes soft-deleted rows
# Instance operations
invoice.delete() # soft delete (excluido=True, excluido_em=now)
invoice.logic_delete(user) # soft delete, records who did it in excluido_por
invoice.reativar() # undo — clears excluido, excluido_em, excluido_porExtends Django's built-in auth.User via a OneToOneField to store auth-related state that doesn't belong on the User model itself. A UserAuth record is created automatically for every new user via a post_save signal — no manual setup required.
Access it via the auth reverse relation:
user.auth.is_email_verified # bool| Field | Type |
|---|---|
user |
OneToOneField to AUTH_USER_MODEL |
is_email_verified |
BooleanField (default: False) |
Country model with 195 pre-loaded entries (see Management Commands).
| Field | Type |
|---|---|
nome |
CharField (unique) |
capital |
CharField |
codigo_3 |
CharField (unique, 3-letter ISO) |
codigo_2 |
CharField (unique, 2-letter ISO) |
Automatically created on every user_logged_in signal via a built-in signal handler. Tracks the user and their IP address.
| Field | Type |
|---|---|
user |
FK to auth.User (nullable) |
ip |
GenericIPAddressField |
Cache for CEP lookups. Populated automatically by the CEP Service.
| Field | Type |
|---|---|
cep |
CharField (unique, formatted XXXXX-XXX) |
logradouro |
CharField |
complemento |
CharField |
bairro |
CharField |
localidade |
CharField |
uf |
CharField |
ibge |
CharField |
ddd |
CharField |
localidade_slug |
SlugField (auto-generated) |
fonte |
CharField (viacep or awesomeapi) |
Tracks images compressed by the compress_images_with_tinify command. Inherits LogicDeletable.
| Field | Type |
|---|---|
path |
CharField |
Looks up Brazilian postal codes with automatic fallback and database caching.
from bodepontoio.services.cep_service import cep_service
dados = cep_service.consultar("01001-000")
# DadosCEP(cep='01001-000', localidade='São Paulo', uf='SP', ...)
# Search cached CEPs by city slug
resultados = cep_service.buscar_por_slug("sao-paulo")Lookup order:
- Local database cache (
ConsultaCEP) - ViaCEP API
- AwesomeAPI (fallback)
Results are saved to the database automatically for future lookups.
Exceptions:
CEPInvalidoError— malformed CEPCEPNaoEncontradoError— not found in any provider
All utilities live under bodepontoio.utils.
from bodepontoio.utils.cleaners import (
file_name_cleaner, # slugify filename, preserve extension
file_extension, # extract file extension
extract_name_and_surname, # split "João Silva" → ("João", "Silva")
get_client_ip, # extract IP from request (X-Forwarded-For aware)
)from bodepontoio.utils.strings import trim_string
trim_string(" hello world ") # "hello world"from bodepontoio.utils.dates import month_to_string
month_to_string(3) # "Março"from bodepontoio.utils.numbers import grana
grana(1234.56) # "1.234,56"
grana(1000, prefixo="R$") # "R$ 1.000,00"Brazilian workday calculator with holidays from 2023 to 2028.
from bodepontoio.utils.workdays import workday, num_workdays
workday(start_date, 5) # 5 business days forward
num_workdays(start_date, end_date) # count business days in rangefrom bodepontoio.utils.email.ofuscate import obfuscate_email
obfuscate_email("[email protected]") # "us**@g****.com"from bodepontoio.utils.forms.fields import ValidatingEmailField
# Django form field that checks MX records (requires dnspython)from bodepontoio.utils.pagination import LastPageFixPaginator
# Paginator that returns the last page instead of raising EmptyPagefrom bodepontoio.utils.database.mysql import Round
# Django Func for ROUND(expr, 2)from bodepontoio.utils.passwords.generate_hash import (
hash_password_pbkdf2, # returns "pbkdf2$iterations$salt_hex$hash_hex"
verify_password_pbkdf2, # constant-time verification
){% load bodepontoio_tags %}
{{ valor|grana }} {# 1.234,56 #}
{{ valor|grana:"R$" }} {# R$ 1.234,56 #}
{{ valor|multiply:2 }} {# valor * 2 #}
{{ valor|roi:custo }} {# ((valor - custo) / custo) * 100 #}
{% url_replace request "page" 2 %} {# preserves other query params #}Optional Sentry metrics wrappers. Requires sentry-sdk (pip install "django-bodepontoio[sentry]").
from bodepontoio.metrics import count, distribution, gauge
count("my_event")
distribution("response_time", 0.45, unit="second")
gauge("queue_size", 42)All calls are wrapped in try/except — they silently log errors if Sentry is unavailable.
Imports 195 countries from the bundled paises.csv into the Pais model.
python manage.py bpio_importar_paisesCompresses images larger than 250 KB using TinyPNG. Requires TINYPNG_KEY in settings and the tinify extra.
pip install "django-bodepontoio[tinify]"
python manage.py compress_images_with_tinify --folders media/uploads/Already-compressed images are tracked in OptimizedImageWithTinyPNG and skipped on subsequent runs.