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 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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4cec0ceb4b56023110799c9dff27c730535d42e10acb87fa44bc377ad271ad3e
|
|
| MD5 |
5a9ce363834c8f29438bb62dcad4f036
|
|
| BLAKE2b-256 |
da7895426b8d784de68436b41f3cc089c42be95dbc8168c945078639b13eae58
|