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"),
# ]

Definições:

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bravaweb-0.0.27.tar.gz (35.6 kB view details)

Uploaded Source

File details

Details for the file bravaweb-0.0.27.tar.gz.

File metadata

  • Download URL: bravaweb-0.0.27.tar.gz
  • Upload date:
  • Size: 35.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for bravaweb-0.0.27.tar.gz
Algorithm Hash digest
SHA256 4cec0ceb4b56023110799c9dff27c730535d42e10acb87fa44bc377ad271ad3e
MD5 5a9ce363834c8f29438bb62dcad4f036
BLAKE2b-256 da7895426b8d784de68436b41f3cc089c42be95dbc8168c945078639b13eae58

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.0.27 This release

1 file

0.0.26

1 file

0.0.25

1 file

0.0.24

2 files

0.0.23

1 file

0.0.22

1 file

0.0.21

1 file

0.0.20

1 file

0.0.19

1 file

0.0.18

1 file

0.0.17

1 file

0.0.16

1 file

0.0.15

1 file

0.0.14

1 file

0.0.13

1 file

0.0.12

1 file

0.0.11

1 file

0.0.10

1 file

0.0.9

1 file

0.0.8

1 file

0.0.7

1 file

0.0.6

1 file

0.0.5

1 file

0.0.4

1 file

0.0.3

1 file

0.0.2

1 file

0.0.1

1 file

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