Introdução ao WSGI

WSGI (Web Server Gateway Interface) é uma especificação de interface que permite a comunicação entre o servidor e sua aplicação python, o PEP 333 descreve em detalhes toda especificação do WSGI 1.0 (existe o PEP 3333 que descreve o WSGI 1.0.1, mas o padrão utilizado atualmente ainda é o 1.0).

O Gunicorn é projeto de maior destaque quando se fala de servidor WSGI nativo, com ele não é preciso de nenhum adaptador WSGI para outro servidor web, apesar disso é bem comum que se utilize um servidor web para servir conteúdo estático e passar o conteúdo dinâmico para o servidor WSGI usando o esquema de proxy reverso.

O mod_wsgi e uwsgi são os maiores projetos em termos de adaptadores WSGI, o mod_wsgi roda em conjunto com o Apache, já o uwsgi roda em conjunto com NGINX.

O grande benefício do WSGI é que já existem servidores/adaptadores bastante eficientes, então você só precisa se preocupar com a parte da aplicação em si, além disso, praticamente todos os frameworks web fazem uso do WSGI o que torna ele o padrão para deploy de aplicações.

Um exemplo de hello world com wsgi

# -*- coding: utf-8 -*-

def hello_world_wsgi(environ, start_response):
    '''
    Uma função simples que demonstra como fazer um hello world com WSGI.
    
    Essa função deve receber dois argumentos environ e start_response.

    O environ contém um dicionário em python com valores preenchidos pelo
    servidor em cada requisição recebida. 
    Para saber mais sobre as variáveis que vem no environ visite: 
        http://www.python.org/dev/peps/pep-0333/#environ-variables
    
    O start_response é um callback enviado pelo servidor para que sua aplicação 
    responda a requisição.

    '''
    # o status segue o padrão do w3:
    # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
    status = '200 OK'
    # response body é o conteúdo que deve ser retornado
    response_body = 'Hello World!'
    # response_headers é o cabeçalho de resposta, geralmente contém o tipo
    # de conteúdo retornado e o tamanho.
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
    # a resposta deve ser iniciada utilizando o start_response
    start_response(status, response_headers)
    # por fim o conteúdo deve ser retornado utilizando alguma estrutura python
    # iterável, nesse caso uma lista foi usada.
    return [response_body]

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, hello_world_wsgi)
    srv.serve_forever()

Claro que esse exemplo pode ser melhorado, vejamos outro exemplo mais elaborado

# -*- coding: utf-8 -*-

def application_handler(environ, start_response):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    Caso a requisição seja para a raíz do site '/' o index_handler vai ser
    carregado, caso contrário o not_found_handler é carregado.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '')
    # verificando se o path é a raíz do site
    if path == '/':
        return index_handler(environ, start_response)
    else:
        return not_found_handler(environ, start_response)

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application_handler)
    srv.serve_forever()

Podemos melhorar a maneira que as urls são chamadas

# -*- coding: utf-8 -*-
import re

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def hello_handler(environ, start_response):
    '''
    Retorna a página hello world do site.
    '''
    status = '200 OK'
    response_body = '<h1>Hello World!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# Lista de urls, cada elemento é uma tupla com dois valores: uma expressão
# regular da url e a função que vai processar a requisição, muito parecido com
# o esquema do django.
urls = [
    (r'^$', index_handler),
    (r'^hello/$', hello_handler),
]

def application_handler(environ, start_response, urls=urls):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    As requisições são despachadas de acordo com a url da lista de urls.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '').lstrip('/')
    # verificando se o path tem algum handler correspondente, caso contrário
    # retorne 404
    for regex, callback in urls:
        match = re.search(regex, path)
        if match is not None:
            return callback(environ, start_response)
    return not_found_handler(environ, start_response)

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application_handler)
    srv.serve_forever()

O código acima tem um pequeno problema, ao acessar o /hello e /hello/ você recebe resultados diferentes, o primeiro retorna 404 enquanto o segundo retorna a página do hello world. Isso acontece por causa do ‘/’ no final da url, seria interessante escrever um middleware que adicionasse essa barra de forma automática se necessário.

# -*- coding: utf-8 -*-
import re

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def hello_handler(environ, start_response):
    '''
    Retorna a página hello world do site.
    '''
    status = '200 OK'
    response_body = '<h1>Hello World!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# Lista de urls, cada elemento é uma tupla com dois valores: uma expressão
# regular da url e a função que vai processar a requisição, muito parecido com
# o esquema do django.
urls = [
    (r'^$', index_handler),
    (r'^hello/$', hello_handler),
]

def fetch_url(url, urls=urls):
    '''
    Função para checar se uma url está listada na lista de urls.

    Retorna o callback em caso positivo e None em caso negativo.
    '''
    url = url.lstrip('/')    
    for regex, callback in urls:
        match = re.search(regex, url)
        if match:
            return callback
    return None

def application_handler(environ, start_response):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    As requisições são despachadas de acordo com a url da lista de urls.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '')
    # verificando se o path tem algum handler correspondente, caso contrário
    # retorne 404
    callback = fetch_url(path)
    if callback:
        return callback(environ, start_response)
    return not_found_handler(environ, start_response)

class AddSlashMiddleware(object):
    '''
    Middleware que verifica se a url requisitada em combinação com o '/' no
    final é válida, em caso afirmativo deve ocorrer um redirecionamento.
    '''
    def __init__(self, application):
        self.application = application
        
    def __call__(self, environ, start_response):
        # Recebe o path
        path = environ.get('PATH_INFO', '')
        if not path.endswith('/'):
            # Se o path não termina com '/', verificar se esse path 
            # combinado com o '/' existe na lista de urls
            callback = fetch_url(path + '/')
            if callback:
                # Redireciona para url
                uri = path + '/'
                start_response('302 FOUND', [('Location', uri)])
                return []
        # Passa o controle para aplicação
        return self.application(environ, start_response)

# cria um application que passa pelo middleware
application = AddSlashMiddleware(application_handler)

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application)
    srv.serve_forever()

Para finalizar, vamos escrever outro middleware, desta vez eu quero que o html retornado fique todo em letra minúscula.

# -*- coding: utf-8 -*-
import re

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def hello_handler(environ, start_response):
    '''
    Retorna a página hello world do site.
    '''
    status = '200 OK'
    response_body = '<h1>Hello World!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# Lista de urls, cada elemento é uma tupla com dois valores: uma expressão
# regular da url e a função que vai processar a requisição, muito parecido com
# o esquema do django.
urls = [
    (r'^$', index_handler),
    (r'^hello/$', hello_handler),
]

def fetch_url(url, urls=urls):
    '''
    Função para checar se uma url está listada na lista de urls.

    Retorna o callback em caso positivo e None em caso negativo.
    '''
    url = url.lstrip('/')    
    for regex, callback in urls:
        match = re.search(regex, url)
        if match:
            return callback
    return None

def application_handler(environ, start_response):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    As requisições são despachadas de acordo com a url da lista de urls.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '')
    # verificando se o path tem algum handler correspondente, caso contrário
    # retorne 404
    callback = fetch_url(path)
    if callback:
        return callback(environ, start_response)
    return not_found_handler(environ, start_response)

class AddSlashMiddleware(object):
    '''
    Middleware que verifica se a url requisitada em combinação com o '/' no
    final é válida, em caso afirmativo deve ocorrer um redirecionamento.
    '''
    def __init__(self, application):
        self.application = application
        
    def __call__(self, environ, start_response):
        # Recebe o path
        path = environ.get('PATH_INFO', '')
        if not path.endswith('/'):
            # Se o path não termina com '/', verificar se esse path 
            # combinado com o '/' existe na lista de urls
            callback = fetch_url(path + '/')
            if callback:
                # Redireciona para url
                uri = path + '/'
                start_response('302 FOUND', [('Location', uri)])
                return []
        # Passa o controle para aplicação
        return self.application(environ, start_response)

class LowercaseMiddleware(object):
    '''
    Middleware que converte o html todo em letras minúsculas.
    '''
    def __init__(self, application):
        self.application = application

    def __call__(self, environ, start_response):
        # Faço uma iteração convertendo o valor obtido para minúsculas
        for chunk in self.application(environ, start_response):
            yield chunk.lower()

# Aplicando o  AddSlashMiddleware
application = AddSlashMiddleware(application_handler)
# Aplicando o LowercaseMiddleware
application = LowercaseMiddleware(application)

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application)
    srv.serve_forever()

Agora temos dois middlewares que são aplicados ao nosso aplicativo, lembrando que a ordem que os middlewares são aplicados é importante.

Como foi visto, o WSGI permite que se criem aplicações de forma bastante simples, recomendo a leitura do Learn WSGI que foi a referência para esse post.