Deploy completo de uma aplicação Django – Mutirão Python

A convite do Marcel da PyCursos, no dia 13/05/2013 eu estive representando a MixinCode na apresentação da palestra Deploy completo de uma aplicação Django.

Desde já, agradeço pelo convite e também ao pessoal que ficou firme e forte até o final da palestra que durou um pouco mais de duas horas.

Abaixo os vídeos e slides.

Vídeo oficial do Hangout:

Vídeo gravado pelo screenflow (alta qualidade):

Slides no slideshare:

Slides no speakerdeck

Django class based views com lazy loading

Vejamos um pequeno exemplo de uso do class based views no Django:

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher
# urls.py
from django.conf.urls import patterns, url
from books.views import PublisherList

urlpatterns = patterns('',
    url(r'^publishers/$', PublisherList.as_view()),
)

O problema desse código é que você precisa importar o PublisherList no urls.py, para pequenos projetos isso não é problema, mas se o projeto for muito grande o tempo de carregamento do urls.py será comprometido.

Usando o paradigma tradicional de views (uma função que recebe request e retorna um HttpResponse) podemos carregar essa view apenas quando necessário, isso é chamado de lazy loading, muito melhor para grandes projetos, pois a importação de todas as views no urls.py é desnecessária.

Podemos resolver isso de uma maneira muito fácil:

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher

publisher_list = PublisherList.as_view()
# urls.py
from django.conf.urls import patterns, url

urlpatterns = patterns('',
    url(r'^publishers/$', 'myapp.views.publisher_list'),
)

Buscando cep diretamente pelo site dos correios em python

Todos que trabalham com desenvolvimento de sistemas já se depararam com o seguinte problema: Como faço para buscar um endereço pelo cep ou vice-versa?

Existe um banco de ceps que pode ser encontrado facilmente usando algum sistema de buscas pela web, o problema é que esse banco é de 2009 e provavelmente desatualizado. Para vergonha dos brasileiros, o site dos correios não provê um webservice oficial para busca de ceps, o máximo que oferece é um form em que os resultados são devolvidos em html mesmo, nada de retorno em json/xml, inclusive os correios te vendem uma base de cep com 900 mil ceps pela pechincha de R$ 1.100,00.

Uma solução viável (até que os correios não botem um captcha na busca) é fazer um screen-scraping em python do resultado html devolvido, para isso já existe uma ótima biblioteca chamada cep que resolve esse problema.

Então eu tive a seguinte ideia, porque não usar o tornado para servir como um webservice do resultado que a biblioteca cep devolve? Eu posso usar o async http cliente do tornado e assim conseguir retornar esses resultados sem bloquear outras requisições que chegam ao meu servidor.

Fazendo um projetinho de 15 minutos eu cheguei no seguinte código (claro, ainda dá pra refatorar muita coisa ainda):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tornado.ioloop
import tornado.web
import tornado.httpclient
import tornado.escape

from urllib import urlencode

# http://pypi.python.org/pypi/cep
from cep import Correios

class MainHandler(tornado.web.RequestHandler):
    
    def get(self):
        self.write(
            '<html><body><form action="/search" method="get">'
            '<input type="text" name="query">'
            '<input type="submit" value="Pesquisar">'
            '</form></body></html>'
        )

class SearchHandler(tornado.web.RequestHandler):
    
    @tornado.web.asynchronous
    def get(self):
        # get search argument
        query = self.get_argument('query', None)
        # write error for empty argument
        if not query or query == '':
            self.write("Por favor preencha o formulário de busca")
            self.finish()
        # create data for request body
        data = dict(
            EndRow='10', 
            Metodo='listaLogradouro', 
            StartRow='1', 
            TipoConsulta='relaxation',
            relaxation=query.encode('cp1252'),
        )
        # create http request
        request = tornado.httpclient.HTTPRequest(
            'http://www.buscacep.correios.com.br/servicos/dnec/consultaLogradouroAction.do',
            method='POST',
            body=urlencode(data, True)
        )
        # create async http client
        http = tornado.httpclient.AsyncHTTPClient()
        # load page
        http.fetch(request, callback=self.process_response)
        
    def process_response(self, response):
        # if error raise status code 500
        if response.error: 
            raise tornado.web.HTTPError(500)
        # cep parsing
        c = Correios()
        resultados = c._parse_tabela(response.body)
        # create json
        json = tornado.escape.json_encode(resultados)
        # output json
        self.set_header('Content-Type', 'application/json')
        self.write(json)
        self.finish()

application = tornado.web.Application([
    tornado.web.url(r'/', MainHandler, name='main'),
    tornado.web.url(r'/search', SearchHandler, name='search'),
])

if __name__ == '__main__':
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Basicamente temos apenas dois handlers, o primeiro é um form com o campo query e o segundo é um handler assíncrono que recebe a query e processa uma request para o site dos correios, ao receber o html eu passo o processamento para a biblioteca cep e finalmente devolvo o resultado em json.

Pronto, agora você pode rodar um webservice local para buscar os ceps diretamente dos correios, lógico que esse projeto pode melhorar bastante, podemos guardar os resultados em um banco de dados evitando que sempre seja feita uma requisição ao site dos correios.

Gerando slugs usando o Django Signals

É muito comum o uso de slugs na construção de urls elegantes, o Django já vem com uma função chamada slugify que transforma um string no formato de slug.

Utilizando o signals, podemos criar uma função que faz o processamento do slug após o registro ser criado no banco, ficando transparente para o usuário final.

Eu vou criar um model chamado category com name e slug

# models.py
# -*- coding: utf-8 -*-
from django.db import models

class Category(models.Model):
    # category name
    name = models.CharField(u'nome', max_length=100)
    # category slug
    slug = models.SlugField(max_length=150, editable=False)

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u'categoria'
        verbose_name_plural = u'categorias'

Note que é interessante usar mais caracteres no max_length do slug, aqui temos o name com max_length=100 e o slug com max_length=150.

Agora para um melhor controle, vamos adicionar outros dois atributos:
slug_field_name: informa qual é o atributo slug
slug_from: informa o atributo de onde o slug vai ser gerado

# models.py
# -*- coding: utf-8 -*-
from django.db import models

class Category(models.Model):
    # category name
    name = models.CharField(u'nome', max_length=100)
    # category slug
    slug = models.SlugField(max_length=150, editable=False)
    # Field to slug
    slug_field_name = 'slug'
    slug_from = 'name'

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u'categoria'
        verbose_name_plural = u'categorias'

Agora vamos escrever a função que vai tratar o signal

# signals.py
# -*- coding: utf-8 -*-
from django.template.defaultfilters import slugify

def create_slug(sender, instance, signal, *args, **kwargs):
    # check for id and attributes
    if instance.id and hasattr(instance, 'slug_field_name') and hasattr(instance, 'slug_from'):
        # get slug information
        slug_name = instance.slug_field_name
        slug_from = instance.slug_from
        # save slug if empty
        if not getattr(instance, slug_name, None):
            # create slug
            slug = '%s-' % instance.id + slugify(getattr(instance, slug_from))
            # set slug
            setattr(instance, slug_name, slug)
            # save instance
            instance.save()

A função create_slug checa se a instância em questão possui id, slug_field_name e slug_from, caso o slug não esteja preenchido um novo slug é gerado e gravado.

Para geração de slugs eu utilizei a concatenação do id do objeto e o slug, por isso que é sempre interessante deixar o max_length do slug maior do que a origem dele. Dessa forma nunca vai existir slugs em duplicidade, porque o id da instância sempre é diferente.

Para finalizar, basta adicionar o signal no models.py

# models.py
# -*- coding: utf-8 -*-
from django.db import models
from django.db.models import signals

from signals import create_slug

class Category(models.Model):
    # category name
    name = models.CharField(u'nome', max_length=100)
    # category slug
    slug = models.SlugField(max_length=150, editable=False)
    # Field to slug
    slug_field_name = 'slug'
    slug_from = 'name'

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u'categoria'
        verbose_name_plural = u'categorias'

# Signals
signals.post_save.connect(create_slug, sender=Category)

Como essa função é genérica, você pode usar ela em qualquer model, basta informar o slug_field_name e slug_from.

Algumas dicas sobre o Django Forms

As dicas desse post foram tiradas da apresentação Advanced Django Forms Usage da DjangoCon, recomendo que todos vejam o vídeo e os slides.

Vídeo:

Slides: http://speakerdeck.com/u/maraujop/p/advanced-django-forms-usage

Esse primeiro exemplo mostra o uso tradicional de um form dentro de uma view do Django:

def my_view(request, template_name='myapp/my_form.html'):

    if request.method == 'POST':
        form = MyForm(request.POST) # Form #1!
        if form.is_valid(): # nested if!
            do_x()
            return redirect('/')
    else:
        form = MyForm() # Form #2!
        
    return render(request, template_name, {'form': form})

O principal problema desse código é que você tem o form declarado duas vezes quando é possível declarar uma única vez. Podemos reescrever esse código dessa forma:

def my_view(request, template_name='myapp/my_form.html'):

    # sticks in a POST or renders empty form
    form = MyForm(request.POST or None)
    if form.is_valid():
        do_x()
        return redirect('home')
    return render(request, template_name, {'form': form})

Agora temos menos linhas de código, o form declarado uma única vez e um if a menos.

No caso de um form com upload de arquivos, podemos fazer assim:

def my_view(request, template_name='myapp/my_form.html'):

    form = MyForm(request.POST or None, request.FILES or None)
    if form.is_valid():
        do_x()
        return redirect('home')
    return render(request, template_name, {'form': form})

Em todo projeto Django é comum que se use bastante os modelforms, nesse caso o uso tradicional é dessa forma:

def my_model_edit(request, slug=slug, template_name='myapp/my_model_form.html'):

    # I wouldn't call the variable model, because it's an instance of a model, it's confusing
    mymodel = get_object_or_404(MyModel, slug=slug)
    if request.method == 'POST':
        form = MyForm(request, instance=mymodel)
        if form.is_valid():
            mymodel = form.save()
            mymodel.day_shown = datetime.datetime.now() # Do any extra model stuff here
            mymodel.save()
            return redirect('home')
    else:
        form = MyForm(instance=mymodel)
    return render(request, template_name, {'form': form, 'model': mymodel})

Aqui temos o mesmo problema do form simples, o form é declarado duas vezes, podemos reescrever dessa forma:

def my_model_edit(request, slug=slug, template_name='myapp/my_model_form.html'):

    mymodel = get_object_or_404(MyModel, slug=slug)
    
    form = MyModelForm(request.POST or None, instance=mymodel)
    
    if form.is_valid():
        mymodel = form.save()
        mymodel.edited_at_djangocon = True
        mymodel.save()
        return redirect('home')
    
    return render(request, template_name, {'form': form, 'mymodel': mymodel})

Novamente temos menos linhas de código, o form declarado uma única vez e apenas um if.

Para um modelform de adição o código ficaria o seguinte:

def my_model_add(request, template_name='myapp/my_model_form.html'):
    
    form = MyModelForm(request.POST or None)
    
    if form.is_valid():
        mymodel = form.save()
        mymodel.added_at_djangocon = True
        mymodel.save()
        return redirect('home')
    
    return render(request,template_name,{'form': form})

Além dessas dicas existem várias outras na apresentação, recomendo mais uma vez a todos que assistam e baixem os slides.

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.