Skip to main content

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 de routes. 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
Pdf 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 o BackgroundTaskManager seja 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)

Source distribution for bravaweb 0.0.28
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page