Um framework faça-você-mesmo
Este artigo é uma tradução livre de um artigo do Python Paste. Você pode encontrar o original aqui.
Ah! Desculpem-me se quebrei alguns códigos, mas senão eles iam ficar «comidos». =/
Sumário
Author: Ian Bicking <mailto:ian@colorstudy.com>
Revisão: 5488
Data: 2006-07-25 17:22:52 -0500 (Ter, 25 Jul 2006)
Tradução: Rodrigo Cacilhas
Data: 2007-07-04
- Introdução e público
- O que é WSGI?
- Escrevendo uma aplicação WSGI
- Agora para um framework
- WSGI middleware
- Configuração
- E depois?
Introdução e público
Este pequeno tutorial pretende ensiná-lo um pouco sobre WSGI e é um exemplo de um pouco da arquitetura que Paste tem permitido e encoraja.
Esta não é uma introdução a tudo sobre Paste – de fato apenas serão usadas e explicadas algumas partes. Também não pretente encorajar todo mundo a sair criando seus próprios frameworks (no entanto honestamente eu não me importaria). A meta é que quando você tiver terminado de ler este artigo você se sinta mais a vontade com frameworks que usem esta arquitetura e um pouco mais seguro por entender o funcionamento interno por trás dos panos.
O que é WSGI?
Em sua forma mais simples WSGI é uma interface entre servidores web e aplicações web. Os mecanismos de WSGI serão explicados abaixo, mas um visão de mais alto nível é dizer que WSGI permite que código passe através de requisições web de forma razoavelmente formal. Mas há mais! WSGI é mais que apenas HTTP. Pode parecer que seja pouco mais que HTTP, mas um pequeno ponto é importante:
- Você passa ao redor de um ambiente tipo CGI, que significa que dados como
REMOTE_USER
(o usuário autenticado) podem ser passados em segurança. - Um ambiente tipo CGI pode ser passado com mais contexto – especificamente em vez de apenas um caminho você tem dois:
SCRIPT_NAME
(como começamos aqui) ePATH_INFO
(o que deixaremos). - Você pode – e muitas vezes deve – colocar suas extensões dentro do ambiente WSGI. Isso permite rechamadas, informações extras, objetos Python arbitrários, ou o que você quiser. Essas são coisas que você não poderia colocar em cabeçalhos HTTP personalizados.
Isso significa que WSGI pode ser usado não apenas entre um servidor web e uma aplicação, mas em todos os níveis de comunicação. Isso permite que aplicações web se tornem mais do que bibliotecas – bem encapsulado, mas ainda assim ricamente reusável.
Escrevendo uma aplicação WSGI
A primeira parte trata de como usar WSGI da forma mais básica. Você pode ler a especificação, mas farei um sumário bastante breve:
- Você escreverá uma aplicação WSGI. Esse é um objeto que responde a requisições. Uma aplicação é apenas um objeto chamável (como uma função) que recebe dois argumentos:
environ
(ambiente) estart_response
(pré-resposta). - O ambiente se parece bastante com um ambiente CGI, com chaves como
REQUEST_METHOD
,HTTP_HOST
, etc. - O ambiente também possui algumas chaves especiais como
wsgi.input
(o fluxo de entrada, como o corpo de uma requisição POST). start_response
é uma função que inicia a resposta – você fornece estado e cabeçalhos aqui.- Por último a aplicação retorna um reiterador com a resposta corpo (geralmente é apenas uma lista de strings ou apenas uma lista contendo uma string com o corpo inteiro).
Então aqui está uma aplicação simples:
def app(environ, start_response):
start_response("200 OK", [
("Content-Type", "text/plain")
])
return ["Ola Mundo!"]
Bem… que sem graça. Certo, você pode imaginar o que ele faz, mas não pode simplesmente chamá-lo do navegador.
Há outras formas mais limpas de fazer isso, mas este tutorial não trata de «limpeza», mas de «clareza». Então apenas adicione ao fim do arquivo:
if __name__ == "__main__":
from paste import httpserver
httpserver.serve(app, host="127.0.0.1", port=8080)
Agora acesse http://127.0.0.1:8080/ e você poderá ver sua nova aplicação. Se quiser entender como um servidor WSGI funciona, recomendo ver CGI WSGI server na especificação WSGI.
Uma aplicação interativa
A última aplicação não foi muito interessante. Vamos ao menos torná-la interativa. Para fazer isso vamos dar-lhe um formulário e então analisar os camos do formulário:
from paste.request import parse_formvars
def app(environ, start_response):
fields = parse_fromvars(environ)
start_response("200 OK", [
("Content-Type", "text/html")
])
if environ["REQUEST_METHOD"] == "POST":
return ["Ola, ", fields["name"], "!"]
else:
return [
'<form method="POST">Nome:',
'<input type="text" name="name" />',
'<input type="submit" />:</form>'
]
A função
parse_fromvars
apenas pega o ambiente WSGI e chama o módulo cgi
(a classe FieldStorage
) e o transforma em um multidicionário.Agora para um framework
Agora isso parece um pouco bruto. Apesar de tudo, estamos testando coisas como
REQUEST_METHOD
para manipular mais do que uma coisa e não está claro como você pode ter mais do que uma página.Nós queremos criar um framework, que é apenas um tipo de aplicação genérica. Neste tutorial implementaremos um publicador de objeto, que é algo que você pode ter visto no Zope, Quixote ou CherryPy.
Publicação de objeto
Num publicador de objeto Python típico você traduz
/
para .
. Então /articles/view?id=5
se torna root.articles.view(id=5)
. Temos de iniciar com algum objeto raiz, claro, que nós passamos…class ObjectPublisher(object):
def __init__(self, root):
self.root = root
def __call__(self, environ, start_response):
…
O método
__call__()
foi sobrescrito para tornar instâncias de ObjectPublisher
objetos chamáveis, exatamente como uma função, e exatamente como uma aplicação WSGI. Agora tudo o que precisamos fazer é traduzir esse environ
para dentro da coisa que estamos publicando, então chamá-la, depois formatar a resposta para WSGI.O caminho
WSGI coloca o caminho requisitado em duas variáveis:
SCRIPT_NAME
e PATH_INFO.SCRIPT_NAME
. É tudo que usaremos. PATH_INFO
é sempre deixado de lado – é a parte que o framework deveria estar usando para encontrar o objeto. Se você colocar os dois juntos de volta, você tem o caminho completo para chegar onde estamos agora mesmo; isso é muito útil para gerar URLs corretas e termos certeza de preservar.Então aqui está como é possível implementar
__call__()
:def __call__(self, environ, start_response):
fields = parse_formvars(environ)
obj = self.find_object(self.root, environ)
response_body = obj(**fields.mixed())
start_response("200 OK", [
("Content-Type", "text/html")
])
return [response_body]
def find_object(self, obj, eviron):
path_info = environ.get("PATH_INFO", "")
if not path_info or path_info = "/":
# Chegamos!
return obj
# PATH_INFO começa sempre com a /, assim vamos nos
# livrar disso:
path_info = path_info.strip("/")
# Então vamos quebrar o caminho no pedaço seguinte,
# e tudo depois dele:
parts = path_info.split("/", 1)
next = parts[0]
if len(parts) == 1:
rest = ""
else:
rest = "/" + parts[1]
# Esconda atributos e métodos privados:
assert not next.startwith("_")
# Agora pegamos os atributos; getattr(a, "b")
# equivale a a.b...
next_obj = getattr(obj, next)
# Agora corrija SCRIPT_NAME e PATH_INFO...
environ["SCRIPT_NAME"] += "/" + next
environ["PATH_INFO"] = rest
# E agora analise a parte restante da URL...
return self.find_object(next_obj, environ)
E é isso, temos um framework.
Levando para um passeio
Agora vamos escrever uma pequena aplicação. Coloque essa classe
ObjectPublisher
no módulo objectpub
:from objectpub import ObjectPublisher
class Root(object):
# O método "index":
def __call__(self):
return """
<form action="welcome">
Nome: <input type="text" name="name" />
<input type="submit" />
</form>
"""
def welcome(self, name):
return "Ola, %s!" % name
app = ObjectPublisher(Root())
if __name__ == "__main__":
from paste import httpserver
httpserver.serve(app, host="127.0.0.1", port=8080)
Tudo certo, feito! Ops, espere. Há ainda algum grande recurso esquecido, por ex. como você ajusta cabeçalhos? E em vez de responder
404 Not Found
em algum lugar, você apenas consegue um erro de atributo. Vamos corrigir essas coisas em uma próxima…Dê-me mais!
Você notará que algumas coisas não estão certas. Mais especificamente, não há como ajustar os cabeçalhos de saída e a informação na requisição está um pouco ligeira.
# Este é apenas um objeto tipo dicionário que possui
# chaves insensíveis ao caso:
from paste.response import HeaderDict
class Request(object):
def __init__(self, environ):
self.environ = environ
self.fields = parse_formvars(environ)
class Response(object):
def __init__(self):
self.headers = HeaderDict(
{ "Content-Type": "text/html" }
)
Agora vou ensinar a você um pequeno truque. Não queremos mudar a assinatura dos métodos. Mas não podemos colocar os objetos de requisição e resposta em variáveis globais normais porque queremos estar preparados para multithreading e todas as threads veem as mesmas variáveis globais (mesmo se estiverem processando requisições diferentes).
Mas Python 2.4 introduz o conceito de «valores locais de thread». É um valor que apenas sua própria thread pode ver. Isso está no objeto
threading.local
. Quando você cria uma instância de local
, qualquer atributo que você ajuste nesse objeto só pode ser visto pela thread onde você ajustou. Então Vamos anexar os objetos de requisição e resposta aqui.Então vamos lembrar-nos de como a função
__call__
se parecia:class ObjectPublisher(object):
…
def __call__(self, environ, start_response):
fields = parse_formvars(environ)
obj = self.find_object(self.root, environ)
response_body = obj(**fields.mixed())
start_response("200 OK", [
("Content-Type", "text/html")
])
return [response_body]
Vamos atualizá-la:
import threading
webinfo = threading.local()
def __call__(self, environ, start_response):
webinfo.request = Request(environ)
webinfo.response = Response()
obj = self.find_object(self.root, environ)
response_body = obj(**webinfo.request.fields)
start_response(
"200 OK",
webinfo.response.headers.items()
)
return [response_body]
Agora em nosso método podemos fazer:
class Root:
def rss(self):
webinfo.response.headers["Content-Type"] = \
"text/xml"
…
Se fôssemos mais estravagantes faríamos coisas como cookies em nosso objeto. Mas não vamos fazer isso agora. Você tem um framework, fique feliz!
WSGI middleware
Middleware é onde as pessoas ficam um pouco intimidadas por WSGI e Paste.
O que é um middleware? É um software que serve de intermediário.
Então vamos escrever um. Vamos escrever um middleware de autenticação.
Vamos usar autenticação HTTP, que também é mistificada. Autenticação HTTP é bem simples:
- Quando autenticação é requerida, devolvemos estado
404 Authentication Required
com o cabeçalhoWWW-Authenticate: Basic realm="Este campo"
- O cliente envia de volta um cabeçalho
Authorization: Basic encoded_info
- O «encoded_info» é um versão base-64 de
usuário:senha
Então como isso funciona? Bem, estamos escrevendo «middleware», o que significa que tipicamente passaremos a requisição para outra aplicação. Podemos mudar a requisição, ou mudar a resposta, mas neste caso às vezes não passaremos a requisição (como quando precisamos dar uma resposta 401).
Para dar um exemplo de middleware muito, muito simples, aqui está um que capitaliza a resposta:
class Capitalizer(object):
# Geralmente passamos a aplicação para ser
# envolvida pelo middleware:
def __init__(self, wrap_app):
self.wrap_app = wrap_app
def __call__(self, environ, start_response):
# Chamamos a aplicação que estamos envolvendo
# com os mesmos argumentos que recebemos...
response_iter = self.wrap_app(
environ,
start_response
)
# Então alteramos a resposta...
response_string = ''.join.(response_iter)
return [response_string.upper()]
Tecnicamente isso não está muito correto, porque há dois jeitos de retornar o corpo de resposta, mas estamos escovando bits.
paste.wsgilib.intercept_output
é uma implementação um tanto mais complexa disso.Então aqui está algum código que faz algo mais útil, autenticação:
class AuthMiddleware(object):
def __init__(self, wrap_app):
self.wrap_app = wrap_app
def __call__(self, environ, start_response):
if not self.authorized(
environ.get("HTTP_AUTHORIZATION")
):
# Basicamente self.auth_required é uma
# aplicação WSGI que apenas sabe como
# responder com with 401...
return self.auth_required(
environ,
start_response
)
# Mas se tudo estiver certo, então passa tudo
# para a aplicação envolvida...
return self.wrap_app(environ, start_response)
def authorized(self, auth_header):
if not auth_header:
# Se eles não deram um cabeçalho, precisam
# autenticar-se...
return False
# .split(None, 1) significa quebrar em duas
# partes nos espaço:
auth_type, encoded_info = \
auth_header.split(None, 1)
assert auth_type.lower() == "basic"
unencoded_info = encoded_info.decode("base64")
username, password = \
unencoded_info.split(":", 1)
return self.check_password(username, password)
def check_password(self, username, password):
# Autenticação não muito segura...
return username == password
def auth_required(self, environ, start_response):
start_response(
"401 Authentication Required",
[
("Content-Type", "text/html"),
("WWW-Authenticate",
'Basic realm="this realm"')
]
)
return ["""
<html>
<head>
<title>Autenticação Requerida</title>
</head>
<body>
<h1>Autenticação Requerida</h1>
Se você não pode entrar, então fique fora.
</body>
</html:gt;"""]
Então como usar isso?
app = ObjectPublisher(Root())
wrapped_app = AuthMiddleware(app)
if __name__ == "__main__":
from paste import httpserver
httpserver.serve(
wrapped_app,
host="127.0.0.1", port=8080
)
Agora você tem um middleware!
Dê-me mais middleware!
É mesmo mais fácil de usar o middleware de outra pessoa do que fazer seu próprio, porque então você não precisa programar. Se você estava seguindo provavelmente, provavelmente encontrou algumas exceções e tem de dar uma olhada no console para ver os avisos de exceção. Vamos tornar um pouco mais simples e mostrar as exceções no navegador…
app = ObjectPublisher(Root())
wrapped_app = AuthMiddleware(app)
from paste.exceptions.errormiddleware \
import ErrorMiddleware
exc_wrapped_app = ErrorMiddleware(wrapped_app)
Fácil! Mas vamos tornar mais interessante…
app = ObjectPublisher(Root())
wrapped_app = AuthMiddleware(app)
from paste.evalexception import EvalException
exc_wrapped_app = EvalException(wrapped_app)
Então cause um erro agora. E acione os pequenos +. E digite qualquer coisa nas caixas.
Configuração
Agora que criamos seu framework e sua aplicação, você pode encontrar a usabilidade juntando algumas dessas partes um tanto cruas. Bem, se você não encontrar, alguém mais que usar sua aplicação e quiser instalá-la em um local diferente ou configurá-la de forma diferente não será bem sucedido.
Então queremos separar o ajuste da aplicação da configuração da aplicação.
E depois?
Fique ligado, falarei sobre configurações (usando Paste Deploy) mais tarde e espero dar uma curta introdução a empacotamente e plugins também. Quando acontecer, aviso em meu blog.
[]'s
Rodrigo Cacilhas