BravaWeb Framework for ASGI Server
Framework para aplicações WEB baseada em Python3 ASGI (Asynchronous Server Gateway Interfac em Uvicorn), com possibilidade de utilização de Template em Html (Mako Templates).
Veja Documentação em:
Uvicorn: https://www.uvicorn.org/ Mako Templates: https://www.makotemplates.org/
Instalação
Instalação utilizando Pip
pip install bravaweb
Git/Clone
git clone https://github.com/robertons/bravaweb
cd bravaweb
pip install -r requirements.txt
python setup.py install
Primeiros Passos
Inicie seu projeto conforme estrutura abaixo
app
├── ...
├── configuration
│ ├── __init__.py
│ └── api.py
└── server.py
O arquivo de configurações deve conter os seguintes dados:
| variável | tipo | obrigatório | descrição |
|---|---|---|---|
| directory | string | sim | Caminho absoluto do projeto |
| encoding | string | sim | Codificação padrão (ex: utf-8) |
| date_format | string | sim | Formato de data completo (ex: %d/%m/%Y %H:%M:%S) |
| short_date_format | string | sim | Formato de data curta (ex: %d/%m/%Y) |
| token | string ou array | sim | Chave secreta para codificação do token JWT no header Authorization. Aceita string única (vale para toda rota) ou lista de tuplas (regex, token) para usar um segredo diferente por rota — veja Autenticação por Rota abaixo |
| domains | array | sim | Domínios autorizados a acessar a API (use ['*'] para liberar todos) |
| access_exceptions | array | sim | Rotas e exceções de controle de acesso |
| routes | array | sim | Rotas do projeto (tuplas de padrão + expressão regular) |
| debug | boolean | não | Quando True, erros retornam JSON com detalhes e traceback (apenas desenvolvimento) |
| trusted_proxies | array | não | IPs de proxies confiáveis para extração do IP real do usuário (use ['*'] para qualquer proxy via CDN) |
| verify_jwt_exp | boolean | não | Quando True, valida o campo exp do token JWT e rejeita tokens expirados |
| response_headers | array | não | Headers HTTP adicionados a todas as respostas (lista de tuplas (bytes, bytes)) |
configuration/__init__.py
# -*- coding: utf-8 -*-
from configuration import api
configuration/api.py
# -*- coding: utf-8 -*-
import os
# Directory
directory = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
# Api Encoding
encoding = "utf-8"
# Date Format
date_format = "%d/%m/%Y %H:%M:%S"
short_date_format = "%d/%m/%Y"
# Token Authorization
token = "JWT-Token-Project"
# Authorized Domains Origin/Referrer
domains = [
"https://www.dominio.com.br",
"https://alias.dominio.com.br",
]
# Exceptions Routes
access_exceptions = [
{'path': '(^/default/)','referer': '*'},
{'path': '(/rota/especifica/)','referer': '(^https://dominio.especifico.com.br/)'},
{'path': '*', 'referer': "(^https://outro.dominio.com.br/)|(^https://adicional.dominio.com.br/)"},
]
routes = [
("{controller}/{area}/{module}/{action}/{id}", '(^/admin/)|(^/panel/)'),
("{controller}/{module}/{action}/{id}", ""),
]
# Optional: enable detailed JSON error responses (development only)
# debug = True
# Optional: trusted proxy IPs for real user IP extraction (CDN/load balancer)
# trusted_proxies = ["*"] # or specific IPs: ["10.0.0.1", "10.0.0.2"]
# Optional: validate JWT expiry
# verify_jwt_exp = True
# Optional: global response headers applied to all routes
# response_headers = [
# (b"Cache-Control", b"no-store"),
# (b"X-Content-Type-Options", b"nosniff"),
# ]
# 0.0.28: field names masked in the request log. The framework has NO built-in
# list — it is the application that knows its own vocabulary. Matched as
# case-insensitive substrings, so each entry is a radical, not a field.
# Without this, masking by name does not happen (a warning is logged once).
log_campos_sensiveis = ["senha", "cpf", "cartao_numero"]
# Optional (0.0.28): size above which any text in the request log is replaced
# by "<N bytes omitted>". Default 512.
# log_limite_texto = 512
Definições:
log_campos_sensiveis (0.0.28): nomes de campo a mascarar no log do request. O framework não traz lista embutida — nome de campo e de coluna pertencem a quem usa a lib, e embutir isso aqui faria o framework carregar dado de domínio alheio e transformaria a correção do campo de um projeto em versão nova para todos.
Conferidos por substring, sem diferenciar maiúscula, então cada entrada é um radical e não um campo: senha cobre nova_senha e senha_atual; cpf cobre cpf_titular. Por isso a lista costuma ser curta.
Sem a lista, o mascaramento por nome não acontece, e o framework registra um aviso uma vez — repetido a cada request viraria ruído e acabaria filtrado. O corte por tamanho (log_limite_texto) continua valendo sem configuração nenhuma.
O valor não é alterado em environment.request: a máscara vale só para o log.
log_limite_texto (0.0.28): tamanho acima do qual qualquer texto no log do request vira <N bytes omitidos>. Padrão 512. É a defesa que pega o anexo em base64 cujo nome de campo ninguém previu — e impede linha de log de megabytes.
domains: lista array de strings, com domínios que tem acesso a api, o teste é feito baseado no origin e/ou referrer de cada requisição.
access_exceptions: é possivel que algumas rotas sejam abertas para qualquer requisição, ou mesmo que alguma rota seja especifica para algum domínio. A lista deve conter um dicionário com as chaves path e referer onde:
path: é referente ao caminho da rota
referer: origem da requisição
Ambos os valores aceitam * para todos ou expressão regular para teste de string.
routes: lista com tuplas que definem as rotas padrões do projeto. Bravaweb esta preparado para até 4 níveis de profundidade que definem, Controlador, Area, Modulo, Ação e mais um nível opcional para captação de ID, a prioridade das regras é sequencial, portanto as regras específicas devem vir primeiro. A tupla é definida assim:
0: a captação de cada parte da profundidade para carregamento
1: expressão regular para identificar a regra
Por padrão os valores de rota do ambiente são:
controller = None
area = None
module = "default"
action="index"
id = None
token: chave secreta usada para assinar/verificar o JWT do header Authorization: Bearer .... Aceita dois formatos:
- string (legado): um único segredo vale para toda a API.
- lista de tuplas
(regex, token): permite um segredo diferente por rota. A primeira regra cuja regex (posição[0]) casar (re.match) contra o path da requisição define o token (posição[1]) usado no decode — mesma lógica sequencial deroutes. Se nenhuma regra casar, a requisição é tratada como não autenticada (environment.bearer = None), igual a um token inválido/expirado.
token = [
('(^/manager/)', secrets_store.JWT_ADMIN_TOKEN),
("", secrets_store.JWT_TOKEN),
]
No exemplo acima, qualquer rota iniciada por /manager/ é validada com JWT_ADMIN_TOKEN; a regra final com regex vazia ("") funciona como catch-all e cobre todas as demais rotas com JWT_TOKEN. A ordem da lista importa — coloque as regras mais específicas primeiro e sempre finalize com um catch-all, senão rotas sem match ficam sem autenticação possível.
Por fim vamos criar a execução do projeto que vai tratar as requisições e processar as rotas.
O arquivo server.py na raiz deve ficar assim:
#-*- coding: utf-8 -*-
import configuration
from bravaweb import App as application
Para uso avançado, é possível passar parâmetros opcionais ao App:
| parâmetro | tipo | descrição |
|---|---|---|
hanndler_error |
callable | callback chamado em erros globais com (exception, scope, headers, context) |
startup_hook |
callable | função síncrona executada durante o lifespan startup, após o pré-carregamento das rotas |
#-*- coding: utf-8 -*-
import functools
import configuration
from bravaweb import App
def on_startup():
# inicializar conexões, caches, etc.
pass
application = functools.partial(App, startup_hook=on_startup)
Acesse o diretório do seu projeto, e execute o comando de serviço do ASGI, conforme documentação do Uvicorn, no exemplo abaixo ativamos o ambiente virtual onde os pacotes estão instalados:
source ../env/bin/activate
uvicorn server:application --port 8080 --interface=asgi3 --workers 7 --proxy-headers --lifespan on --reload
O Resultado então será:
INFO: Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)
INFO: Started reloader process [82503] using statreload
INFO: Started server process [82505]
.
.
.
Neste momento sua aplicação estará em execução. Nós configuramos as rotas mas não desenvolvemos nenhuma delas portanto qualquer requisição na url http://127.0.0.1:8080 irá retornar 404.
Hello World
Vamos iniciar aplicando a rota default, a pasta do projeto nesse momento deverá estar assim:
app
├── ...
├── configuration
│ ├── __init__.py
│ └── api.py
├── controllers
│ └── default.py
└── server.py
Conforme exemplificado a rota default(padrão) é
controller = None
area = None
module = "default"
action="index"
id = None
O arquivo ficará assim:
controllers/default.py
# -*- coding: utf-8 -*-
from bravaweb.controller import *
class DefaultController(Controller):
@get
async def index(self) -> Json:
await View(self.environment, data={"mensagem": 'Olá Mundo'})
Analisando a rota default:
Nome do Controlador é default, por isso nome da classe é DefaultController, herdando o controlador do framework (Controller)
O metodo de request aceito para esta rota é o GET (@get) , mas POST (@post) , PUT(@put) e DELETE(@delete) também são aceitos. Uma requisição diferente do permitido para rota retorna Erro 405: Method not allowed
O Framwork é baseado em ASGI (Asynchronous Server Gateway Interface) por isso ação index é assíncrona (async) .
A anotação é o tipo de resultado que essa rota irá retornar, posteriormente veremos sobre os tipos, no exemplo acima utilizamos Json.
Todos os dados da requisição, estão na environment, veremos mais logo a seguir.
Para melhor compreenção sobre as rotas , vejamos os exemplos abaixo baseado no arquivo de configuração acima:
Criando e Configurando Rotas
Os padrões de rota é configurado no arquivo de configurações em routes. Você provavelmente fará isso somente uma vez, ou quando for necessária a criação de rotas específicas em seu projeto. Abaixo segue alguns exemplos baseado na configuração que apresentamos.
Exemplo 1
GET -> api.dominio.com.br/admin/catalog/products/list
A regra identificada é a primeira da lista, pois o path da request inicia com /admin/ conforme expressão regular da posição [1] da tupla em configuration.api.routes:
("{controller}/{area}/{module}/{action}/{id}", '(^/admin/)|(^/panel/)'
O resultado da captação da rota conforme posição [0] da tupla será:
controller = 'admin'
area = 'catalog'
module = 'products'
action = 'list'
A estrutura para processamento desta rota devera ser:
app
├── ...
├── controllers
│ └── admin
│ │ └── catalog
│ │ | └── products.py
O arquivo :
controllers/admin/catalog/products.py
# -*- coding: utf-8 -*-
from bravaweb.controller import *
class ProductsController(Controller):
@get
async def list(self) -> Json:
await View(self.environment, data=[{"prod_nome": 'Exemplo'}])
Exemplo 2
POST -> api.dominio.com.br/site/product/like/110
A regra identificada é a default (segunda da lista), pois o path da request não contempla as expressões regulares anteriores :
("{controller}/{module}/{action}/{id}", "")
O resultado da captação da rota conforme posição [0] da tupla será:
controller = 'site'
area = None
module = 'product'
action = 'like'
id = 110
A estrutura para processamento desta rota devera ser:
app
├── ...
├── controllers
│ └── site
│ │ └── product.py
O arquivo:
controllers/site/product.py
# -*- coding: utf-8 -*-
from bravaweb.controller import *
class ProductController(Controller):
@post
async def like(self) -> Json:
await View(self.environment, data=[{"likes": 535}])
Ambiente / Environment
A qualquer momento dentro do controlador é possivel acessar os dados da requisição através de self.environment os dados disponíves são:
| Campo | Tipo | descrição |
|---|---|---|
| origin | string | Origem ou Referrer da Requisição |
| remote_ip | string | IP do usuário (extraído do proxy real se trusted_proxies configurado) |
| remote_uuid | string | UUID se informado no header |
| browser | string | Browser do usuário |
| accept_encoding | string | Tipos de codificação aceitos pelo browser |
| method | string | Método da requisição (GET, POST, PUT ou DELETE) |
| response_type | class | Tipo de resposta esperada para requisição |
| authorization | string | Token JWT - Bearer enviado no Header |
| bearer | dict | Token JWT decodificado (payload) |
| auth_token | string | Token de autorização a ser retransmitido na resposta |
| content_length | int | Tamanho da requisição |
| get | dict | Dados enviados por querystring |
| post | dict | Dados enviados por POST (JSON ou form-data) |
| request | dict | União de get e post — todos os parâmetros da requisição |
| body | bytes | Bytes brutos do corpo da requisição |
| remote | dict | Dicionário com ip, uuid, browser, accept |
| route | list | Caminho da rota como lista de segmentos |
| controller | string | Nome do controlador |
| area | string | Nome da área do controlador |
| module | string | Nome do módulo do controlador |
| action | string | Nome da ação do módulo |
| id | string | Identificador da requisição |
Há disponível também, para casos de manipulação específica os dados brutos do ASGI:
| Campo | descrição |
|---|---|
| headers | cabeçalho da requisição |
| scope | escopo da requisição |
| send | conexão com navegador |
| receive | dados recebidos |
Entradas e Pré-condições
Para maior segurança no processamento das rotas é possível e recomendável estabelecer as pré-condições daquela rota específica. Caso a requisição não tenha o objeto ou objeto informado seja inválido, haverá erro de resposta com erro 412: Precondition Failed
@post
async def comment(self, id_product:int, comment:string ) -> Json:
sql_query = f"INSERT INTO products_comments (prod_comment, id_product) VALUES ('{comment}',{id_product})";
.
.
.
await View(self.environment, data=[{"added": true}])
Caso a request não contenha os parametros acima, a ação não será executada.
É possível requerer objetos específicos, Bravaweb realiza o cast automático dos dados enviados, no caso datetime o parametro de conversão esta estabelecido no arquivo de configuração nos campos date_format e short_date_format.
from datetime import datetime
from decimal import Decimal
.
@post
async def comment(self, id_product:int, comment:string, date:datetime, stars:Decimal) -> Json:
.
.
.
.
.
.
await View(self.environment, data=[{"added": true}])
View
Toda rota deve retornar uma view, que será baseada na anotação a action.
await View(self.environment, data=_response_data)
Bravaweb possui tratamento específico para respostas Json e HTML, ambos possuem um modelo ou carregamento de template para resposta.
A View possui os seguintes campos de entrada
| entrada | obrigatório | tipo | descrição |
|---|---|---|---|
| environment | sim | bravaweb.environment | ambiente da requisição |
| data | sim | bytes-like, dict, list, string | dados da resposta de acordo com a anotação |
| success | não | boolean | sucesso na execução da action |
| token | não | string | auth token; se omitido e houver token no environment, o mesmo se repete |
| error | não | dict, list, string | mensagem de erro |
| headers | não | list of tuples | headers customizados para esta resposta; substitui o response_headers global do config |
Anotações e Tipos de Resposta
| Tipo | Entrada |
|---|---|
| Html | dict |
| Css | bytes-like object |
| Csv | bytes-like object |
| JavaScript | bytes-like object |
| Jpg | bytes-like object |
| Json | dict, list, string |
| Mp4 | bytes-like object |
| bytes-like object | |
| Png | bytes-like object |
| TextPlain | bytes-like object |
| Webm | bytes-like object |
| Xml | bytes-like object |
Json
O template Json é composto da seguinte forma:
Json = { "token": "", "success": True, "date": "", "items": 0, "data": [], }
Onde os dados respondidos estarão dentro de "data".
@get
async def index(self) -> Json:
await View(self.environment, data=[{"added": true}])
HTML e Template Mako
Para mais informações sobre a criação de templates Mako acesse: https://www.makotemplates.org/
A estrutura das Views HTML desenvolvidas em Mako devem estar assim:
app
├── ...
├── configuration
├── controllers
├── views
│ └── shared
│ | └── default.html
└── server.py
Quando não há uma view definida para rota, o template padrão a ser carregado será o default.
é possível criar views específicas para cada rota conforme exemplo abaixo:
Rota: /product/detail
@get
async def index(self) -> Html:
await View(self.environment, data=[{"added": true}])
Template:
app
├── ...
├── configuration
├── controllers
├── views
│ └── product
│ | └── detail
│ | | └── index.html
│ └── shared
└── server.py
Execução em Segundo Plano (Background Tasks)
O módulo bravaweb.background oferece um gestor de tarefas em segundo plano que resolve o problema de I/O síncrono (banco de dados, envio de emails, chamadas HTTP, processamento de arquivos) bloqueando o event loop ASGI.
A instância global background é inicializada automaticamente no lifespan startup e encerrada graciosamente no shutdown.
from bravaweb.background import background
Métodos disponíveis
| método | descrição |
|---|---|
await background.run_io(func, *args) |
Executa função síncrona em thread pool e aguarda o resultado. Use quando a resposta depende do resultado. |
background.submit_io(func, *args) |
Fire-and-forget em thread pool. A requisição retorna imediatamente enquanto a tarefa roda em background. |
background.get_semaphore(name, limit) |
Retorna semáforo asyncio nomeado para controle de concorrência em código async. |
background.get_thread_semaphore(name, limit) |
Retorna semáforo threading nomeado para uso dentro de funções executadas via submit_io/run_io. |
Exemplos
run_io — aguarda o resultado (query de banco):
from bravaweb.controller import *
from bravaweb.background import background
class ProductController(Controller):
@get
async def detail(self, id: int) -> Json:
produto = await background.run_io(db.products.where(id=id).first)
await View(self.environment, data=produto)
submit_io — fire-and-forget (envio de email):
@post
async def buy(self, id_product: int) -> Json:
background.submit_io(send_confirmation_email, id_product)
await View(self.environment, data={"ordered": True})
get_semaphore — controle de concorrência (processamento pesado):
@post
async def analyze(self, image_path: str) -> Json:
async with background.get_semaphore("analyzer", limit=3):
result = await background.run_io(heavy_analyzer.run, image_path)
await View(self.environment, data=result)
Nota: O lifespan precisa estar ativo (
--lifespan on) para que oBackgroundTaskManagerseja inicializado corretamente. Se o lifespan não estiver ativo, o gestor inicializa automaticamente de forma lazy na primeira chamada (com aviso no log).
Headers de Resposta Customizados
É possível adicionar headers HTTP customizados tanto globalmente (para todas as rotas) quanto por rota específica.
Headers globais — configurar em configuration/api.py:
response_headers = [
(b"Cache-Control", b"no-store"),
(b"X-Content-Type-Options", b"nosniff"),
]
Headers por rota — passar o parâmetro headers ao View():
@get
async def download(self, file_name: str) -> Pdf:
_data = open(file_name, 'rb').read()
await View(
self.environment,
data=_data,
headers=[(b"Content-Disposition", f"attachment; filename={file_name}".encode())]
)
Headers passados diretamente ao
View()substituem os headers globais de mesmo nome.
Decoradores
Bravaweb é compatível com encapsulamento através de decorador e a criação deve seguir o modelo abaixo:
Decorador de Método Síncrono:
def decorator_example(f):
def example_decorator(cls, **args) -> f:
return f(cls, **args)
return example_decorator
Decorador de Método Assíncrono:
def decorator_example_async(f):
async def example_decorator(cls, **args) -> f:
return await f(cls, **args)
return example_decorator
O uso do decorador em um método síncrono ficaria assim:
@decorator_example
def __init__(self):
.
.
O uso do decorador em uma rota ficaria assim:
@decorator_example_async
async def index(self) -> Html:
await View(self.environment, data=_response_data)
É possível também criar decorar para um controlador inteiro, a função "decora" todos os métodos executáveis, observe que os métodos padrões de classe init e del são métodos síncronos e por isso o decorador síncrono, e demais métodos (actions) com decorador assíncrono.
def decorator_example_klass():
def decorate(cls):
for attr in cls.__dict__:
_method = getattr(cls, attr)
if hasattr(_method, '__call__'):
if attr == "__init__" or attr == "__del__":
setattr(cls, attr, example_decorator(_method))
else:
setattr(cls, attr, decorator_example_async(_method))
return cls
return decorate
Erros:
A qualquer momento no processamento da sua rota é possível responder com as seguintes mensagens de erro:
| Método | Código de Resposta | Comportamento |
|---|---|---|
| NoContent | 204 | Resposta vazia |
| Unauthorized | 401 | Acesso não autorizado |
| NotFound | 404 | Recurso não encontrado |
| NotAllowed | 405 | Método HTTP não permitido para a rota |
| PreconditionFailed | 412 | Parâmetros ausentes ou inválidos; em modo debug, retorna JSON com detalhes dos erros |
| InternalError | 500 | Erro interno; em modo debug, retorna JSON com traceback completo |
Todos os métodos são assíncronos (async) e devem ser chamados com await.
Exemplo requisição de um arquivo pdf:
import os.path
@get
async def index(self, file_path: str) -> Pdf:
if os.path.exists(file_path):
_file_data = open(file_path, 'rb')
await View(self.environment, data=_file_data.read())
else:
await self.NotFound()
License
MIT
Metadata
Release files for bravaweb 0.0.28
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| bravaweb-0.0.28.tar.gz | 42.7 kB | Without provenance | Details for bravaweb-0.0.28.tar.gz |
Release files / bravaweb-0.0.28.tar.gz
| Download URL | bravaweb-0.0.28.tar.gz |
|---|---|
| Size | 42.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
db22babfd865bfec86df33a8472e0737ad124870246fa41c500653ae2994d491
|
|
BLAKE2b-256 checksum How to use checksums |
c5cbba8f60e51311bf5a0b7da09653330e32943e4ecf9e4703d58a0acbaa078c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Security for bravaweb 0.0.28
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.