2007-06-04

Um framework faça-você-mesmo

Poliedro 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


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) e PATH_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) e start_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çalho WWW-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.

Ian Bicking





[]'s
Rodrigo Cacilhas