Usando o Docker para deploy de aplicações Django

Nessa última sexta eu tive a oportunidade de apresentar mais um mutirão python, dessa vez eu falei sobre o Docker.io e como fazer um deploy de um pequena aplicação Django usando o conceito de containers do Docker.

Vídeo do hangout:

Vídeo gravado com screenflow:

Slides:


Django + Docker

Evento:
Usando o Docker para deploy de aplicações Django

Vídeos do Mutirão Python Construindo um micro framework web em Python

Ontem eu tive a oportunidade de falar sobre a minha experiência em criar o micro framework Gunstar, como de costume eu sempre gravo o hangout com o screenflow e recomendo assistir essa versão.

Vídeo do hangout

Vídeo gravado com screenflow

Slides:
http://www.slideshare.net/allisson/micro-frameworkpython

http://speakerdeck.com/allisson/construindo-um-micro-framework-web-em-python

Gunstar 0.2

Acabei de liberar o release 0.2 do Gunstar, a principal mudança foi na documentação, que está bem mais abrangente, agora temos vários tópicos e não apenas o quickstart.

Tive o prazer de fechar a issue #1 do projeto também 😀

Para os interessados, agora em setembro eu estarei fazendo um mutirão python sobre o Gunstar, se você quiser saber mais sobre WSGI e como é simples criar um micro framework, não perca!

Atualização:
A versão 0.2 está com um bug na instalação, eu já liberei a 0.2.1 que resolve o problema. Eu agradeço ao Elton Santos por avisar.

É difícil criar um micro web framework em python? Eu consegui em 12 dias :)

Hoje eu fiz o lançamento da versão 0.1.0 do Gunstar, um micro web framework em python. Isso levou ao todo 12 dias, desde a criação do repositório até a publicação da versão 0.1.0 no pypi.

A principal facilidade em escrever um micro framework em tão pouco tempo se resume a um padrão , o WSGI, com ele podemos escrever aplicações usando ótimas bibliotecas como o WebOb (que usei no Gunstar) ou o Werkzeug (base do Flask). Essas duas bibliotecas cuidam de transformar requisição e resposta em objetos que posso manipular facilmente na minha aplicação python.

Com o tempo que poupei usando o WebOb eu escrevi duas classes que fazem o papel de roteamento de urls, o código é bem simples, e o melhor de tudo, funciona!

Também criei uma interface para gerir sessão dos usuários usando signed cookies, não vejo motivos para usar sessão em arquivos ou em banco de dados, claro que temos que reconhecer que existe um limite para o tamanho da sessão e que isso tem uma influência no tamanho das requisições e respostas, mas se você precisa guardar tanta informação assim em uma sessão, você está fazendo isso errado, grave apenas a chave na sessão e o resto retorne de um banco de dados.

Usei o Blinker para implementar signals, um dos usos foi na captura do template e do contexto em que esse template foi renderizado para usar nos testes, o código chega a ser ridículo, o blinker é uma mão na roda.

A documentação ainda está devendo em muitos pontos, temos apenas o quickstart por enquanto, mas se vocês tiverem curiosidade podem olhar os testes que vão descobrir o que podemos fazer com o Gunstar.

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.