Crônica de um bug anunciado
- publicado em 02 de abril de 2023
Este texto eu tinha começado a escrever em 2015. Agora voltando o blog, pegando os backups achei e acho que tá bom de terminar.
O contexto da história
O software livre, pouco mais de uma década depois do seu “surgimento formal” teve seu racha que continua até hoje. Por um lado um grupo que de denominava de software livre e outro que se denominava como código aberto. Estes enfatizavam a superioridade técnica sobre o modelo de desenvolvimento fechado enquanto aqueles enfatizavam as liberdades advindas do compartilhamento de código, mas que apesar das diferenças acabavam convivendo bem. As coisas começaram a degringolar por volta de 2007, com as GPL v3 wars que culminaram com um diminuição do uso da GPL e com as licenças mais permissivas tendo um destaque maior (muitos projetos agora colocam uma licença permissiva como uma feature do software).
Assim chegamos a um ponto onde o pessoal do software livre se sente como tendo sido derrotado em certo sentido porque apesar do uso de software livre ter sido disseminado enormemente ao ponto de hoje em dia o software livre ser parte do mainstream do desenvolvimento de software nos dias de hoje, o que acabou se disseminando na verdade foi o open source, para ser mais preciso, a ideologia open source, já que o software é basicamente o mesmo.
Como começou?
A formação do movimento do software livre sempre se deu mais ou menos nas seguintes premissas: Um grupo (ou um indivíduo só) de hackers desenvolvia algum programa, e principalmente dos anos 90 em diante, colaborando via internet, fazendo com que o desenvolvimento de software livre pudesse crescer de maneira exponencial. O principal elo entre estes hackers era, obviamente, o software sendo produzido, mas além disso também uma ideologia compartilhamento de conhecimento, de bens imateriais no geral e uma certa apologia à liberdade, seja lá o que isso for. E ao mesmo tempo em que crescia o software livre, cresciam também as oportunidades de negócio e os contatos com as grandes empresas de software proprietário.
A questão de modelos de negócios já existia no movimento de software livre, com Richard Stallman falando sobre isso no manifesto gnu e pensando nos modelos de negócio propostos no manifesto gnu podemos começar a entender o porquê da primeria disputa entre capitalistas envolvendo o software livre: de um lado as nascentes empresas de internet e de outro as empresas de software proprietário. Entenda-se disputa aqui de uma maneira bem ampla, não sendo exatamente uma disputa, mas sim uma diferença de relacionamento com o software livre ditadas por questões econômicas. As empresas de internet desde o seu nascimento usaram de software livre vendo aí uma alternativa viável, melhor e mais barata aos softwares proprietários (linux + apache foi o primeiro «sucesso» do linux).
Do lado da comunidade de software livre, ao mesmo tempo, também começaram a se ressaltar as primeiras divergências, agora entre o pessoal mais “apolítico”, mais preocupado com software e o pessoal mais “ideológico”, preocupado com software, mas sempre ressaltando (basicamente) a questão do usuário como o controlador do seu ambiente computacional. O primeiro grupo com o passar do tempo começou a se chamar de open source ao invés de free software. A princípio essa divisão era apresentada - principalmente pelos propomentes do open source - como uma diferença menor, uma diferença no modo de se lidar com “os gravatas”, mas no fim era tudo a mesma coisa, todos usavam e escreviam os mesmos programas, usavam as mesmas licensas, eram todos parte da mesma coisa.
O que pegou?
As coisas continuaram mais ou menos assim até a metade/final dos anos 2000 até que foi lançada a GPLv3. O objetivo dessa nova versão da GPL é tapar os buracos na redação da gpl v2 e que deixavam brechas para usos proprietários de programas sob a gpl. Mas com a chegada da gpl v3 as diferenças entre open source e software livre se tornaram mais visíveis e estas diferenças acabaram sendo a expressão mais visível de um movimento para tirar da sala qualquer coisa que pudesse trazer pruridos numa reunião com executivos. Nessa época as licensas mais permissivas começaram a ser mais usadas, a gpl perdeu relevância e o software livre virou “oficialmente” open source. Curiosamente, em certo sentido, as “previsões” de ambos, software livre e open source se concretizaram.
Com o avanço das licensas permissivas, as empresas perderam o “medo de contaminação” com a gpl e software open source se tornou cada vez mais presente até ser padrão em muitos lugares. Mais do que isso, software livre é a espinha dorsal da internet, rodando em servidores e roteadores e atualmente também a base de muito software desenvolvido em cima de bibliotecas livres.
Oito anos depois
Foi aqui que parei de escrever em 2015 e depois de oito anos a marcha dos acontecimentos continuou acelerada no caminho que estava. Software livre faz parte do mainstream do desenvolvimento de software corporativo. Em qualquer projeto de software hoje em dia você vai encontrar muito software livre ou no software em si ou na tool chain, em algum lugar você vai encontrar algum código livre. O principal dispositivo computacional das pessoas hoje em dia é um telefone. A maioria dos telefones roda android e android é feito em cima de um monte de coisa livre (mas não gpl porque o google tem medo de infecção). Mas ao mesmo tempo o ambiente computacional no telefone é uma das coisas mais travadas que há além de estar cheio de aplicativos proprietários. Como?
No ponto de se ter um monte de coisas proprietárias feitas em cima de software livre é o resultado da proliferação de licensas sem copyleft. Licensas com copyleft tem uma caracteristica principal que é fazer com que todos participem do jogo. «Tá aqui meu código, pra participar da brincadeira você também contribui com código». Já licensas sem copyleft simplesmente dão as 4 liberdades, mas não demandam código em troca. E quando você dá um monte de coisas pra um negócio sem demandar nada em troca, não vai haver troca. Negócios são negócios.
Por outro lado a maioria das pessoas usa seus dispositivos geralmente como um terminal burro, toda a computação é feita em servidores e os dispositivos mostram o resultado e se nem a gpl v3 ganhou tração imagina a agpl que foi feita pra lidar exatamente com servidores. Então a gente chegou ao ponto onde nunca se teve tanto software livre sendo feito e usado e ao mesmo tempo o ambiente computacional das pessoas nunca foi tão travado.
Por que o bug era anunciado?
O movimento de software livre causou movimentos muito fortes e causou avanços incríveis na indústria de software. Nos anos 80 um compilador de c custava alguns milhares de dólares, nos anos 90 todo ambiente de desenvolvimento era com software proprietário e hoje as coisas são bem diferentes. Mas apesar disso as empresas de tecnologia estão no controle da informação e da computação com maior preponderância ainda. Aconteceram flame wars de todo tipo, posts gigantescos pseudo-filosóficos sobre o que é liberdade e no final das contas o que importava mesmo pros negócios é o quanto de trabalho grátis elas conseguem extrair.
O caso do software livre foi um caso de se olhar pras árvores e não ver a floresta. Enquanto mirávamos em como fazer nossos programas melhores e mais úteis, a gente esqueceu que programas de computador são só ferramentas e ferramentas são sempre usadas por pessoas dentro de um contexto. O nosso contexto é que a produção de qualquer coisa (inclusive de programas de computador) que sirva para atender alguma necessidade humana está submetida à logica da maximização e apropriação de lucros.
Por esta lógica, ter o controle dos dados, da computação, dos algoritmos usados por todos e cada vez mais importatantes é extremamente lucrativo e enquanto estivermos submetidos a este contexto, as nossas licensas de software podem ganhar muitas batalhas, mas a guerra vai sempre ser perdida.
Apresentando: MongoMotor
- publicado em 17 de agosto de 2018
Oi, pessoal. Tudo certo?
A ideia hoje é falar um pouquinho sobre o MongoMotor, que é uma biblioteca para acesso assíncrono ao MongoDB usando Python.
O que é esse tal de assíncrono mesmo?
Em geral, operações de entrada e saída de dados (io) consomem bastante tempo aguardando a chegada/envio de dados e enquanto um programa aguarda estas operações todo o processamento é bloqueado e nada mais será feito até que os dados sejam recebidos ou enviados. Para lidar com esta situação, as soluções comuns são threads e multi-processos (vide apache) ou io assíncrono, utilizando eventos do sistema operacional (vide nginx).
Com io assíncrono, quando executamos alguma operação de io ao invés de aguardarmos o retorno, a operação é «deixada de lado» (num scheduler), liberando o código para processar outras coisas, e quando obtivermos alguma resposta da operação de io o sistema operacional enviará um evento informando que a resposta chegou e assim o processamento da operação original pode ser retomado.
Em Python, há muito tempo se tem projetos que usam a ideia de io assíncrono para resolver este problema, como o Tornado ou o Twisted, mas com a chegada do módulo asyncio à biblioteca padrão no Python 3.4 e com a inclusão da super simpática sintaxe async/await no Python 3.5, operações de io assíncrono se tornaram uma coisa muito mais corriqueira na linguagem.
Para uma super palestra sobre concorrência, veja este vídeo. E esse async/await, hem?
Bem resumidamente, com async podemos definir corotinas em Python. Corotinas são funções que quando chamadas retornam um objeto Future e precisam ter a sua execução agendada por algum io loop. Algo assim:
async def my_coro():
"""Uma corotina que não faz nada"""
# do something
return 'some result'
async def other_coro():
"""Uma corotina que chama outra corotina"""
# r será o valor retornado por my_coro depois que sua execução for terminada.
# Se chamássemos `my_coro()` sem `await` r seria uma future.
r = await my_coro()
# faça algo com r
return 'a new result'
# Agora agendamos a future retornada `other_coro()`
loop = asyncio.get_event_loop()
loop.run_until_complete(other_coro())
Para mais informações veja: asyncio docs e leia esse post.
Finalmente o MongoMotor
O mongomotor tira proveito do Motor, um driver assíncrono para MongoDB, e do MongoEngine, com sua api à la Django ORM, criando assim uma biblioteca bem simples para acesso assíncrono ao mongodb. Basta criar classes representando seus documentos, declarar alguns atributos - se quiser - e é isso, já podemos acessar o mongodb de maneira assíncrona.
Código. Aleluia!
Acabou a parte chata e chegou o que todo mundo queria: código. Neste exemplo, vamos analisar as perguntas mais recentes no stackoverflow.
A primeira coisa que precisamos fazer é instalar o mongomotor. Isto é feito usando-se o pip. Num terminal digite o seguinte:
$ pip install mongomotor
Agora, com o mongomotor já instalado podemos começar a escrever o código. Num arquivo python, faça:
# -*- coding: utf-8 -*-
# A função connect é usada para connectar a um banco de dados
from mongomotor import connect
# Conectamos uma vez quando nosso programa começa e está feia a conexão.
# Usando connect() sem parâmetros, o mongomotor vai tentar se conectar ao
# mongo em localhost na porta 27017, o padrão para a instalação.
connect()
# Se necessário é possível passar outros parâmetros, além dos parâmetros de
# autenticação
connect(host='my.mongo.host', port=1234, username='myself', password='my-password')
Agora vamos definir os nossos documentos:
# A classe Document é a base para os nossos documentos que serão definidos
from mongomotor import Document
# Apesar de o mongo ser um banco de dados sem schema, usamos estes campos
# para declarar nosso schema ficando mais fácil o entendimento posterior
# do código.
# Documentos com campos dinâmicos podem ser criados Usando-se a classe
# mongomotor.DynamicDocument
from mongomotor.fields import URLField, StringField, ListField, ReferenceField, IntField
class Usuario(Document):
"""Um usuário que fez uma pergunta no stackoverflow."""
# Este campo será um inteiro e é obrigatório, por isso o uso do
# parâmetro required=True.
# Usamos também o parâmetro unique=True para garantir que só exista
# um documento com este valor.
external_id = IntField(required=True, unique=True)
"""O id do usuário no so."""
nome = StringField()
"""O nome usuário que será exibido. O nome, não o usuário. :P"""
reputacao = IntField()
"""A reputação do usuário no site."""
class Pergunta(Document):
external_id = IntField(required=True, unique=True)
"""O id da pergunta no so."""
titulo = StringField(required=True)
"""O título da pergunta"""
# URLField é uma string que será validada para verificar se é uma
# url
url = URLField(required=True, unique=True)
"""A url da pergunta no so."""
# ReferenceField aponta para um outro documento.
# NOTA: Esta relação é feita na apliação, não no mongodb server.
usuario = ReferenceField(Usuario, required=True)
"""O usuário que fez a pergunta."""
# ListField indica que o campo é uma lista. Neste caso teremos uma lista
# de strings.
tags = ListField(StringField())
"""A lista de tags da pergunta"""
# Para que o unique funcione precisamos criar os índices nas coleções.
Usuario.ensure_indexes()
Pergunta.ensure_indexes()
Pronto, nossos documentos já estão definidos. Para informações sobre todas as opções para definir documentos, veja aqui.
Agora podemos inserir dados e fazer buscas nos documentos. Para inserir os dados vamos user a api do stackoverflow. Para fazer requisições http assíncronas, usaremos a biblioteca aiohttp. Num terminal instale-a com:
$ pip install aiohttp
Aqui a função para baixar os dados.
import json
# Usamos aiohttp para fazer requests http assíncronos
from aiohttp import ClientSession
SO_URL = 'https://api.stackexchange.com/2.2/questions?order=desc&sort=activity&site=stackoverflow'
async def get_so_questions():
async with ClientSession() as session:
async with session.get(SO_URL) as response:
r = await response.read()
# Retorna uma lista de dicionários. Cada dicionário contém informação
# sobre uma pergunta.
return json.loads(r.decode())['items']
O uso do aiohttp não está no escopo deste artigo, mas a ideia aqui é fazer as operações de io (no caso os requests http) de maneira assíncrona.
Agora já podemos cadastrar alguns dados. Primeiro vamos criar um método para criar um usuário baseado na informação retornada pela api.
class Usuario(Document):
"""Um usuário que fez uma pergunta no stackoverflow."""
# Este campo será um inteiro e é obrigatório, por isso o uso do
# parâmetro required=True.
# Usamos também o parâmetro unique=True para garantir que só exista
# um documento com este valor.
external_id = IntField(required=True, unique=True)
"""O id do usuário no so."""
nome = StringField()
"""O nome de usuário que será exibido"""
reputacao = IntField()
"""A reputação do usuário no site."""
# Adicionamos este método para inserir usuários.
@classmethod
async def get_or_create(cls, user_info):
"""Retorna um usuário. Tenta obter um usuário através de sua
external_id. Se não existir, cria um novo usuário.
:param user_info: Um dicionário com informações do usuário enviado
pela api.
"""
external_id = user_info['user_id']
nome = user_info['display_name']
reputacao = user_info['reputation']
try:
# O atributo `objects` é um objeto do tipo QuerySet.
# O método `get()` retorna um documento baseado nos parâmetros
# passados a este método.
user = await cls.objects.get(external_id=external_id)
except cls.DoesNotExist:
# Quando nenhum documento que se enquadra nos parâmetros
# é encontrado uma exceção `DoesNotExist` é levantada.
# Aqui neste caso criamos um novo documento
user = cls(external_id=external_id, nome=nome,
reputacao=reputacao)
# E salvamos o documento usando o método `save()`
await user.save()
return user
Agora vamos escrever um pouco de código para inserir as perguntas baseado no retorno da api
class Pergunta(Document):
"""Uma pergunta feita no stackoverflow."""
external_id = IntField(required=True, unique=True)
"""O id da pergunta no so."""
titulo = StringField(required=True)
"""O título da pergunta"""
# URLField é uma string que será validada para verificar se é uma
# url
url = URLField(required=True, unique=True)
"""A url da pergunta no so."""
# ReferenceField aponta para um outro documento.
# NOTA: Esta relação é feita na apliação, não no mongodb server.
usuario = ReferenceField(Usuario, required=True)
"""O usuário que fez a pergunta."""
# ListField indica que o campo é uma lista. Neste caso teremos uma lista
# de strings.
tags = ListField(StringField())
"""A lista de tags da pergunta"""
@classmethod
async def adicionar_perguntas(cls, perguntas):
"""Adiciona as perguntas retornadas pela api.
:param perguntas: Uma lista de dicionários, cada um com informações
sobre uma pergunta.
"""
# Lista para armazenar as perguntas a medida em que formos criando
# os documentos para salvá-las todas de uma vez só.
instancias = []
for pinfo in perguntas:
# Primeiro criamos usuário
usuario = await Usuario.get_or_create(pinfo['owner'])
# Agora criamos a pergunta
external_id = pinfo['question_id']
url = pinfo['link']
title = pinfo['title']
tags = pinfo['tags']
pergunta = cls(external_id=external_id, url=url, titulo=title,
tags=tags, usuario=usuario)
# Adicionamos à lista de instâncias para serem salvas depois
instancias.append(pergunta)
# Agora salvamos todas as instâncias de uma vez só.
await cls.objects.insert(instancias)
E uma função pra juntar tudo e popular o banco de dados.
async def populate_db():
"""Função para popular o banco de dados com as últimas perguntas do
stackoverflow.
"""
# Vamos limpar tudo primeiro
await Pergunta.drop_collection()
await Usuario.drop_collection()
# Agora cadastramos as perguntas mais recentes
perguntas = await get_so_questions()
await Pergunta.adicionar_perguntas(perguntas)
Bom, depois de inserir alguns dados no banco, vamos fazer buscas nestes dados.
async def stats():
"""Função que mostra alguns dados obtidos através da api do stackoverflow.
"""
# O método `count()` é usado para contar a quantidade de documentos
# em um queryset.
total_perguntas = await Pergunta.objects.count()
total_usuarios = await Usuario.objects.count()
print('Temos um total de {} perguntas de {} usuários diferentes\n'.format(
total_perguntas, total_perguntas))
# Podemos usar o método `order_by()` para ordenar os resultados.
# Note que não é preciso o uso de await quando estamos filtrando/ordenando
# um queryset. A operação de io só é executada quando um documento for
# necessário
usuarios = Usuario.objects.order_by('-reputacao')
# Usamos método `fisrt` para pegar o primeiro resultado do queryset.
# Aqui sim é necessário o uso de await.
usuario = await usuarios.first()
print('O usuário com maior reputação é: *{}* com reputação {}'.format(
usuario.nome, usuario.reputacao))
# Podemos usar o método `filter()` para filtrar os resultados de um
# queryset
fileterd_qs = Pergunta.objects.filter(usuario=usuario)
# E podemos iterar sobre os resultados do queryset com `async for`
print('As perguntas de *{}* são:'.format(usuario.nome))
async for pergunta in fileterd_qs:
print('- {}'.format(pergunta.titulo))
print(' tags: {}'.format(', '.join(pergunta.tags)))
print('')
# Com o método `item_frequencies()` podemos contas as repetições
# de items de listas em documentos de um queryset
popular_tags = await Pergunta.objects.item_frequencies('tags')
tags = sorted([(k, v) for k, v in popular_tags.items()],
key=lambda x: x[1], reverse=True)
most_popular = tags[0]
print('A tag mais popular é *{}* com {} perguntas'.format(most_popular[0],
most_popular[1]))
# Podemos filtar um queryset com base em um item de uma lista, no
# nosso exemplo, com uma tag
print('As perguntas de *{}* são:'.format(most_popular[0]))
async for pergunta in Pergunta.objects.filter(tags=most_popular[0]):
print('- {}'.format(pergunta.titulo))
print(' tags: {}'.format(', '.join(pergunta.tags)))
# Note que para acessar uma referência é necessário o uso
# de await
usuario = await pergunta.usuario
print(' usuario: {}'.format(usuario.nome))
print('')
Para a documentação completa de como fazer buscas usando o mongomotor veja aqui.
E é isso. Acabamos de conhecer o básico do mongomotor. Pra finalizar, vamos fazer uma função que coloca tudo isso junto:
async def main():
print('Populando o banco de dados...')
await populate_db()
print('Banco de dados populado!\n')
await stats()
E por fim, colocar isso aqui no final do nosso arquivo:
if __name__ == '__main__':
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
E feito! Nosso script final ficou assim:
# -*- coding: utf-8 -*-
import json
from aiohttp import ClientSession
from mongomotor import connect
from mongomotor import Document
from mongomotor.fields import (URLField, StringField, ListField, ReferenceField,
IntField)
connect()
SO_URL = 'https://api.stackexchange.com/2.2/questions?order=desc&sort=activity&site=stackoverflow'
async def get_so_questions():
async with ClientSession() as session:
async with session.get(SO_URL) as response:
r = await response.read()
return json.loads(r.decode())['items']
class Usuario(Document):
"""Um usuário que fez uma pergunta no stackoverflow."""
# Este campo será um inteiro e é obrigatório, por isso o uso do
# parâmetro required=True.
# Usamos também o parâmetro unique=True para garantir que só exista
# um documento com este valor.
external_id = IntField(required=True, unique=True)
"""O id do usuário no so."""
nome = StringField()
"""O nome de usuário que será exibido"""
reputacao = IntField()
"""A reputação do usuário no site."""
# Adicionamos este método para inserir usuários.
@classmethod
async def get_or_create(cls, user_info):
"""Retorna um usuário. Tenta obter um usuário através de sua
external_id. Se não existir, cria um novo usuário.
:param user_info: Um dicionário com informações do usuário enviado
pela api.
"""
external_id = user_info['user_id']
nome = user_info['display_name']
reputacao = user_info['reputation']
try:
# O atributo `objects` é um objeto do tipo QuerySet.
# O método `get()` retorna um documento baseado nos parâmetros
# passados a este método.
user = await cls.objects.get(external_id=external_id)
except cls.DoesNotExist:
# Quando nenhum documento que se enquadra nos parâmetros
# é encontrado uma exceção `DoesNotExist` é levantada.
#
# Aqui neste caso criamos um novo documento
user = cls(external_id=external_id, nome=nome,
reputacao=reputacao)
# E salvamos o documento usando o método `save()`
await user.save()
return user
class Pergunta(Document):
"""Uma pergunta feita no stackoverflow."""
external_id = IntField(required=True, unique=True)
"""O id da pergunta no so."""
titulo = StringField(required=True)
"""O título da pergunta"""
# URLField é uma string que será validada para verificar se é uma
# url
url = URLField(required=True, unique=True)
"""A url da pergunta no so."""
# ReferenceField aponta para um outro documento.
# NOTA: Esta relação é feita na apliação, não no mongodb server.
usuario = ReferenceField(Usuario, required=True)
"""O usuário que fez a pergunta."""
# ListField indica que o campo é uma lista. Neste caso teremos uma lista
# de strings.
tags = ListField(StringField())
"""A lista de tags da pergunta"""
@classmethod
async def adicionar_perguntas(cls, perguntas):
"""Adiciona as perguntas retornadas pela api.
:param perguntas: Uma lista de dicionários, cada um com informações
sobre uma pergunta.
"""
# Lista para armazenar as perguntas a medida em que formos criando
# os documentos para salvá-las todas de uma vez só.
instancias = []
for pinfo in perguntas:
# Primeiro criamos usuário
usuario = await Usuario.get_or_create(pinfo['owner'])
# Agora criamos a pergunta
external_id = pinfo['question_id']
url = pinfo['link']
title = pinfo['title']
tags = pinfo['tags']
pergunta = cls(external_id=external_id, url=url, titulo=title,
tags=tags, usuario=usuario)
# Adicionamos à lista de instâncias para serem salvas depois
instancias.append(pergunta)
# Agora salvamos todas as instâncias de uma vez só.
await cls.objects.insert(instancias)
# Para que o unique funcione precisamos criar os índices nas coleções.
Usuario.ensure_indexes()
Pergunta.ensure_indexes()
async def populate_db():
"""Função para popular o banco de dados com as últimas perguntas do
stackoverflow.
"""
# Vamos limpar tudo primeiro
await Pergunta.drop_collection()
await Usuario.drop_collection()
# Agora cadastramos as perguntas mais recentes
perguntas = await get_so_questions()
await Pergunta.adicionar_perguntas(perguntas)
async def stats():
"""Função que mostra alguns dados obtidos através da api do stackoverflow.
"""
# O método `count()` é usado para contar a quantidade de documentos
# em um queryset.
total_perguntas = await Pergunta.objects.count()
total_usuarios = await Usuario.objects.count()
print('Temos um total de {} perguntas de {} usuários diferentes\n'.format(
total_perguntas, total_perguntas))
# Podemos usar o método `order_by()` para ordenar os resultados.
# Note que não é preciso o uso de await quando estamos filtrando/ordenando
# um queryset. A operação de io só é executada quando um documento for
# necessário
usuarios = Usuario.objects.order_by('-reputacao')
# Usamos método `fisrt` para pegar o primeiro resultado do queryset.
# Aqui sim é necessário o uso de await.
usuario = await usuarios.first()
print('O usuário com maior reputação é: *{}* com reputação {}'.format(
usuario.nome, usuario.reputacao))
# Podemos usar o método `filter()` para filtrar os resultados de um
# queryset
fileterd_qs = Pergunta.objects.filter(usuario=usuario)
# E podemos iterar sobre os resultados do queryset com `async for`
print('As perguntas de *{}* são:'.format(usuario.nome))
async for pergunta in fileterd_qs:
print('- {}'.format(pergunta.titulo))
print(' tags: {}'.format(', '.join(pergunta.tags)))
print('')
# Com o método `item_frequencies()` podemos contas as repetições
# de items de listas em documentos de um queryset
popular_tags = await Pergunta.objects.item_frequencies('tags')
tags = sorted([(k, v) for k, v in popular_tags.items()],
key=lambda x: x[1], reverse=True)
most_popular = tags[0]
print('A tag mais popular é *{}* com {} perguntas'.format(most_popular[0],
most_popular[1]))
# Podemos filtar um queryset com base em um item de uma lista, no
# nosso exemplo, com uma tag
print('As perguntas de *{}* são:'.format(most_popular[0]))
async for pergunta in Pergunta.objects.filter(tags=most_popular[0]):
print('- {}'.format(pergunta.titulo))
print(' tags: {}'.format(', '.join(pergunta.tags)))
# Note que para acessar uma referência é necessário o uso
# de await
usuario = await pergunta.usuario
print(' usuario: {}'.format(usuario.nome))
print('')
async def main():
print('Populando o banco de dados...')
await populate_db()
print('Banco de dados populado!\n')
await stats()
if __name__ == '__main__':
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
O script pode ser baixado aqui.
Salve isto em um arquivo chamado mmso.py e num terminal execute:
$ python mmso.py
E voilà, eis a saída do nosso programa:
Populando o banco de dados...
Banco de dados populado!
Temos um total de 30 perguntas de 30 usuários diferentes
O usuário com maior reputação é: *jww* com reputação 49941
As perguntas de *jww* são:
- Undefined reference to symbol during link GCC inline assembly
tags: c++, gcc, linker-errors, inline-assembly
A tag mais popular é *python* com 5 perguntas
As perguntas de *python* são:
- BeautifulSoup4 findChildren() is empty
tags: python, html, parsing, beautifulsoup
usuario: Meghan M.
- How to update weights in neural networks?
tags: python, neural-network
usuario: Absolute Idiot
- assign results to dummy variable _
tags: python, python-3.x
usuario: cs0815
- use opencv, cv2.videocapture in kivy with android - python for android
tags: android, python, opencv, kivy, buildozer
usuario: Vajira Prabuddhaka
- How to get the value of a Django Model Field object
tags: python, django, django-models
usuario: Hugo Luis Villalobos Canto
Para informações mais detalhadas sobre o mongomotor, veja a documentação.
O mongomotor é software livre, sinta-se à vontade para contribuir. :)
Comporte-se, menino! Testando suas aplicações com behave
- publicado em 21 de novembro de 2016
Boas, pessoal! Tudo tranquilo? Hoje vamos falar sobre como testar o comportamento de suas aplicações, fazendo testes de aceitação, usando um carinha chamado behave.
Testes de aceitação? Behave? WTF!?
Testes de aceitação são testes que verificam a correção de alguma funcionalidade do seu programa, geralmente feitos a partir de user stories definidas durante o planejamento da sessão de desenvolvimento. Estes testes validam os resultados esperados do sistema, do ponto de vista de um usuário.
O behave é uma biblioteca que te permite escrever seus testes em linguagem natural (as user stories) e escrever as ações dos testes usando a linguagem Python.
Uma aplicaçãozinha de exemplo
Suponhamos que temos uma pequena aplicação web que mostra o horário o horário atual quando acessamos a página. O código pra isso seria algo assim:
# -*- coding: utf-8 -*-
# arquivo mostra_hora.py
import datetime
from flask import Flask
def get_formated_datetime():
dt = datetime.datetime.now()
formated = dt.strftime('%d/%m/%Y %H:%M:%S')
return formated
app = Flask(__name__)
@app.route('/')
def index():
tmpl = "<html><head><title>Que horas são?</title></head>"
tmpl += '<body><form action="/hora"><input type="submit"" value="click!"/>'
tmpl += '</body></html>'
return tmpl
@app.route('/hora')
def show_datetime():
tmpl = "<html><head><title>Agora são...</title></head>"
tmpl += '<body>O horário atual é:{}</body></html>'
dt = get_formated_datetime()
return tmpl.format(dt)
if __name__ == '__main__':
app.run()
Com essa aplicaçãozinha pronta, podemos executar o seguinte comando para iniciar a aplicação:
$ python mostra_hora.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Agora, quando você acessar, no seu navegador, o endereço 127.0.0.1:5000 você vai ver uma página com um botão. Clicando nesse botão você cai em outra página em que a data/hora atual são mostradas.
Os testes
Agora vamos escrever os nossos testes. A primeira coisa que temos que fazer é escrever uma user story contendo os passos que um usuário executaria no nosso site. A história fica em um arquivo com extensão .feature, então criaremos um arquivo chamado tests/site.feature com o seguinte conteúdo:
Feature: Ver a data e hora atual no site
Scenario: Navegar até ver o data e hora.
Given o usuário acessou o site
When ele clica no botão 'click!'
Then ele vê a data e hora atual.
Como vocês podem ver, este arquivo .feature descreve em linguagem natural os passos que seriam executados por um usuário no site que deseja usar a funcionalidade sendo executada. Vamos parar por aqui e ver em detalhes o fizemos neste arquivo.
Given, When, Then & friends
As linhas com Given, When e Then são os passos que serão executados durante o teste. A diretiva “given” é usada para deixar o sistema em um estado conhecido para que a partir deste estado possamos executar os testes. Nesta diretiva você poderia criar dados, executar alguns passos ou qualquer outra coisa que seja precisa para deixar o seu teste pronto para execução. A diretiva “When” é usada para executar alguma ação e a diretiva “Then” é onde verificamos o resultado do passo executado em “When”.
Além destas diretivas também temos as diretivas “And” e “But” que são diretivas feitas somente para facilitar a escrita dos testes. Quando usarmos uma diretiva “And” or “But”, o behave traduzirá estas diretivas como sendo a última diretiva usada. Então, quando usamos When… And… este And é traduzido para um When pelo behave na hora da execução dos testes.
O ambiente
Em, geral, antes de começarmos os nossos testes precisamos configurar o ambiente com algumas coisas que precisamos que já estejam rodando assim que começarmos os testes. No caso da nossa aplicação de teste precisamos que o servidor já esteja de pé e precisamos de uma instância de um browser selenium para podermos simular o comportamento do usuário. Para configurar o ambiente, o behave usa um arquivo chamado environment.py. O nosso environment.py seria algo assim:
# -*- coding: utf-8 -*-
from selenium import webdriver
from mostra_hora import runserver, killserver
def before_feature(context):
"""Função executada antes cada feature dos testes. O contexto
passado aqui é compartilhado por todos os steps, então se criarmos
algo e colocarmos como um elemento do contexto podemos recuperar o
que criamos dentro de um step a partir do contexto."""
# primeiro subimos o servidor
runserver()
# e depois criamos um browser e deixamos no contexto
context.browser = webdriver.Chrome()
def after_feature(context):
"""Função executada depois de cada feature dos testes."""
# agora matamos o servidor
killserver()
# fechamos o browser
context.browser.quit()
Os steps
Agora que já temos nossa funcionalidade descrita no arquivo .feature precisamos escrever um código Python que contém as ações a serem executadas durante os testes. Os passos serão associados ao com base no texto do passo descrito no arquivo .feature. Então vamos criar um arquivo chamado tests/steps/site_steps.py com o seguinte conteúdo:
# -*- coding: utf-8f -*-
from behave import given, when, then
@given(u'o usuário acessou o site')
def step_impl(context):
"""Acessamos a página inicial do site"""
context.browser.get('http://127.0.0.1:5000')
@when(u"ele clica no botão 'click!'")
def step_impl(context):
"""clicamos no botão"""
context.browser.find_element_by_tag_name('input').click()
@then(u'ele vê a data e hora atual.')
def step_impl(context):
"""Verificamos que a data e hora é realmente exibida."""
browser = context.browser
is_present = u'O horário atual é' in browser.page_source
assert is_present
Agora já temos tudo o que precisamos para executar os testes. Isso é feito usando o comando behave e apontando para um diretório que contenha os arquivos .feature, assim:
$ behave tests/
Feature: Ver a data e hora atual no site # tests/site.feature:1
Scenario: Navegar até ver o data e hora # tests/site.feature:3
Given o usuário acessou o site # tests/site.feature:4
When ele clica no botão 'click!' # tests/site.feature:5
Then ele vê a data e hora atual. # tests/site.feature:6
1 features passed, 0 failed, 0 skipped
1 scenarios passed, 0 failed, 0 skipped
3 steps passed, 0 failed, 0 skipped, 0 undefined
E aí temos o relatório da execução dos testes indicando o que passou, o que falhou, onde e etc.
Bom, esta foi só uma pequena introdução ao behave, para saber mais veja a documentação oficial.
Empacotamento e distribuição de projetos Python sem mistério - Adendo
- publicado em 15 de setembro de 2015
Oi, pessoal. Hoje o post é rapidinho, só um adendo ao post anterior sobre empacotamento de projetos Python.
A coisa é a seguinte: A maneira recomendada para fazer o upload do pacote para o pypi agora é usando o twine. Esse carinha é o recomendado agora porque, diferentemente do pip, ele faz o upload usando https.
Então, quando formos empacotar e distribuir nosso programa, ao invés de usarmos:
$ python setup.py sdist upload
Para criar o pacote e subi-lo, usaremos o comando sdist para criar o pacote e depois usaremos o twine para fazer o upload, assim:
$ python setup.py sdist
$ twine upload dist/my-package-0.1.tar.gz
E é isso!
Empacotamento e distribuição de projetos Python sem mistério
- publicado em 06 de março de 2015
Oi, pessoal, tudo certo? O assunto hoje é o empacotamento dos nossos projetos Python, ou seja, como a gente faz pra distribuir o nosso código pra outras pessoas, isso de uma maneira fácil pra nós que desenvolvemos e pra quem vai instalar o programa. A ideia aqui é explicar o que precisa fazer pra o nosso programa poder ser instalado via pip, assim facilitando a distribuição.
Começando
O que a gente precisa fazer pra empacotar e distribuir nosso projeto é bem pouca coisa. Antes de começar a gente só precisa das dependências, o setuptools e o pip. Instalando o pip o setuptools vem como dependência. No debian o pacote chama python-pip, e até acho que vem por padrão, nos red hat é algo assim também. Em outros OSs não sei como instalar, mas vai lá, instala rapidão. Aproveita e instala o virtualenvwrapper que a gente vai usar pra testar as coisas.
Já instalou? Beleza, agora vamos lá.
Um exemplo simples
Pra começar vamos usar um exemplo bem simples, o nosso programa vai ser um único módulo, ou seja, ,um único arquivo .py. Vamos criar um diretório vazio pra gente trabalhar:
$ mkdir ~/dist-teste
$ cd ~/dist-teste
É aqui neste diretório que a gente vai criar módulo (um arquivo.py) que queremos distribuir. Vamos criar um módulo chamado meusuperprograma (arquivo meusuperprograma.py). Aqui tem que se tomar cuidado pra não escolher um nome que já seja de algum módulo da biblioteca padrão do Python ou que já seja de alguma outra biblioteca popular (uma busca no pypi.python.org ajuda). Então, aqui está o nosso módulo de exemplo:
#-*- coding: utf-8 -*-
# arquivo meusuperprograma.py
import time
def faz_algo_dahora():
return time.time()
Tendo isto, um programa que é um único módulo, nossa estrutura de diretórios ficou assim:
dist-teste/
`-- meusuperprograma.py
Já temos um programa, agora a gente precisa ajeitar as coisas pra distribuir. Pra isso precisamos criar um arquivo chamado setup.py no mesmo nível de diretório que o nosso módulo (no diretório ~/dist-teste). Neste arquivo que vão estar as configurações para o empacotamento. Um setup.py básico seria assim:
#-*- coding: utf-8 -*-
from setuptools import setup
setup(name='meusuperprograma', # aqui o nome do seu programa
version='0.1', # a versão.
author='Eu Mesmo',
author_email='me@myplace.net',
# Esta url deveria ser a url para a documentação/código/site oficial do projeto.
url='http://meusuperprograma.org',
# Aqui uma lista dos módulos que compõe a sua distribuição.
# No nosso caso, um módulo só.
py_modules=['meusuperprograma'],
)
Então, agora com o setup.py temos a seguinte estrutura de diretórios:
dist-teste/
|-- setup.py
`-- meusuperprograma.py
Com isso já podemos distribuir nosso programa. Só precisamos subir nosso código para o CheeseShop e todo mundo vai poder instalar com um simples pip install. Legal, né? Mas peraí… O que é mesmo o CheeseShop, hem?
CheeseShop, o Python Package Index
CheeseShop é o codinome secreto do Python Package Index, aquele carinha que você encontra em https://pypi.python.org/pypi e tenho certeza que você já conhece. Quando a gente instala um programa com pip install… é aí que o pip vai procurar o programa. Além deste pypi, a gente ainda tem um pypi de teste à nossa disposição, esse aqui: https://testpypi.python.org/pypi. Vai lá, se registra (nos dois, são bases separadas) e volta aqui. Rápido.
Pronto? Beleza. Agora a gente vai configurar o pip pra usar as nossas credenciais. No arquivo ~/.pypirc coloque o seguinte:
[distutils]
index-servers =
pypi
testpypi
[pypi]
username: ze
password: ninguém
[testpypi]
username: ze
password: ninguém
repository: https://testpypi.python.org/pypi
E é isso. Já temos o nosso super programa pra distribuir, já temos nosso arquivo de configuração da distribuição (o setup.py) e já estamos registrados nos lugares pra onde queremos subir nosso código. Agora é só alegria.
Distribuindo nosso programa
Como essa é a primeira versão do nosso programa, a gente vai precisar registrar nosso projeto. A gente faz isso com o comando register do setuptools. No exemplo abaixo registraremos nosso programa no pypi de teste, por isso usaremos o parâmetro -r testpypi para indicar que usaremos o repositório que está com o nome testpypi no nosso .pypirc. Se não usássemos este parâmetro, iriamos registrar no pypi oficial. Então, pra registrar fica assim:
$ python setup.py register -r testpypi
running register
running egg_info
[ output cortado ]
running check
Registering meusuperprograma to https://testpypi.python.org
Server response (200): OK
Agora que já registramos nosso programa, podemos fazer um release, isto é, fazer o upload de uma versão do nosso código. A gente faz isso com os comandos sdist e upload. O comando sdist cria uma distribuição com os nossos arquivos e o upload envia este arquivo para o servidor escolhido. Novamente usaremos o parâmetro -r testpypi.
$ python setup.py sdist upload -r testpypi
running sdist
running egg_info
[ output cortado ]
warning: sdist: standard file not found: should have one of README, README.rst, README.txt
running check
[ output cortado ]
Creating tar archive
removing 'meusuperprograma-0.1' (and everything under it)
running upload
Submitting dist/meusuperprograma-0.1.tar.gz to https://testpypi.python.org/pypi
Server response (200): OK
E é isso, temos nosso programa prontinho pra distribuir - apesar do warining por causa da falta de README.
Testando nossa distribuição
Pra testar a nossa distribuição, criaremos um virtualenv e também criaremos um diretório vazio para ser nosso diretório de trabalho nos testes. O diretório vazio é para nada “ficar no caminho” e atrapalhar nos testes. Então, vamos criar as coisas primeiro:
$ mkvirtualenv meusuperprogramaenv -p /usr/bin/python3.4
[ output cortado ]
$ mkdir ~/dir-limpo && cd ~/dir-limpo
Agora, vamos instalar nosso programa e testar pra ver se foi tudo instalado. Repare que será usado o parâmetro –index-url para indicar que o pip deve procurar pelo pacote no CheeseShop de teste.
$ pip install meusuperprograma --index-url=https://testpypi.python.org/pypi
[ output cortado ]
Successfully installed meusuperprograma
Cleaning up...
$ python
Python 3.4.2 (default, Oct 8 2014, 10:45:20)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import meusuperprograma
>>> meusuperprograma.faz_algo_dahora()
1417391400.7311318
>>>
É isso, nosso programa foi instalado corretamente pelo pip. Mas ainda tem mais coisas pra gente ver.
Um programa com packages
O nosso primeiro exemplo foi bem simples, um programa com apenas um módulo, mas agora nosso programa cresceu ao invés de um módulo temos dois, e pra organizar tudo isso vamos colocá-los dentro de um package. Um package é simplesmente um diretório que contém módulos python.
A estrutura do nosso programa com package ficou assim:
dist-teste/
|-- COPYING
|-- meusuperprograma/
| |-- __init__.py
| |-- modulo_a.py
| `-- modulo_b.py
|-- README
`-- setup.py
Além de alterarmos a estrutura do nosso programa também incluímos um arquivo README (info sobre o programa, docs etc) e um arquivo COPYING com a lincença do programa. Aqui está o conteúdo dos nossos módulos.
Arquivo meusuperprograma/modulo_a.py:
#-*- coding: utf-8 -*-
# arquivo meusuperprograma/modulo_a.py
import time
def faz_algo_dahora():
return time.time()
Arquivo meusuperprograma/modulo_b.py
# -*- coding: utf-8 -*-
# arquivo meusuperprograma/modulo_b.py
import datetime
def faz_algo_sensacional(timestamp):
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.strftime('%H:%M:%S - %d/%m/%Y')
Arquivo meusuperprograma/__init__.py
# -*- coding: utf-8 -*-
# arquivo meusuperprograma/__init__.py
from meusuperprograma.modulo_a import faz_algo_dahora
from meusuperprograma.modulo_b import faz_algo_sensacional
def faz_algo_sensacionalmente_dahora():
timestamp = faz_algo_dahora()
datahora = faz_algo_sensacional(timestamp)
return {'timestamp': timestamp,
'datahora': datahora}
E com isto, temos um programa com um package para distribuir. Vamos fazer algumas alterações no nosso setup.py para darem conta da nova versão do nosso programa.
#-*- coding: utf-8 -*-
from setuptools import setup
setup(name='meusuperprograma', # aqui o nome do seu programa
version='0.2', # temos que alterar a versão.
author='Eu Mesmo',
author_email='me@myplace.net',
url='http://meusuperprograma.org',
# ao invés de usarmos o parâmetro py_modules usamos
# o parâmetro packages.
packages=['meusuperprograma'],
# Vamos colocar também alguns classificadores. Estes classificadores
# não são obrigatórios, mas deus gosta mais de você quando você
# classifica seus programas.
# Você pode ver uma lista com todos os classificadores aqui:
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Natural Language :: Portuguese',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Topic :: Software Development :: Libraries :: Python Modules',
],
)
Assim, já podemos fazer o release desta nova versão do programa.
$ cd ~/dist-teste
$ python setup.py sdist upload -r testpypi
running sdist
running egg_info
[ output cortado ]
running check
[ output cortado ]
Submitting dist/meusuperprograma-0.2.tar.gz to https://testpypi.python.org/pypi
Server response (200): OK
Agora, vamos testar esta distribuição da nova versão
Testando a distribuição com packages
Vamos atualizar a versão do meusuperprograma que está instalado no nosso virtualenv de teste e vamos ao diretório limpo para testar se foi mesmo instalado corretamente. Note que vamos usar uma opção nova, o parâmetro –upgrade que diz para o pip atualizar a versão caso já haja alguma instalada. Não esqueça de ativar seu virtualenv antes de atualizar a versão.
$ # ative o virtualenv se não estiver ativado
$ workon meusuperprogramaenv
$ cd ~/dir-limpo
$ pip install meusuperprograma --index-url=https://testpypi.python.org/pypi --upgrade
[ output cortado ]
Successfully installed meusuperprograma
Cleaning up...
$ python
Python 3.4.2 (default, Oct 8 2014, 10:45:20)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import meusuperprograma
>>> meusuperprograma.faz_algo_sensacionalmente_dahora()
{'timestamp': 1417399828.9924762, 'datahora': '00:10:28 - 01/12/2014'}
>>>
E tudo certo, nosso programa com package foi instalado corretamente.
O nosso programa ficou tão legal, tão sensacionalmente dahora que a gente decidiu criar um script para o nosso programa poder ser chamado diretamente da linha de comando, como um programa qualquer que a gente usa.
Um programa com script
Para o nosso programa ter um script que qualquer um pode usar da linha de comando, simplesmente criaremos, no nosso root dir do programa, um diretório chamado scripts e dentro deste diretório colocaremos o script que queremos que os usuários executem, e no nosso caso será um script chamado meusuperprograma (sem o .py mesmo).
A estrutura de diretórios do nosso programa com este novo script ficou assim:
/home/juca/dist-teste
|-- COPYING
|-- meusuperprograma
| |-- __init__.py
| |-- modulo_a.py
| `-- modulo_b.py
|-- README
|-- scripts
| `-- meusuperprograma
`-- setup.py
E este é o conteúdo do arquivo scripts/meusuperprograma
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# arquivo scripts/meusuperprograma
import sys
from meusuperprograma.modulo_a import faz_algo_dahora
from meusuperprograma.modulo_b import faz_algo_sensacional
if __name__ == '__main__':
if len(sys.argv) > 1:
timestamp = float(sys.argv[1])
else:
timestamp = faz_algo_dahora()
datahora = faz_algo_sensacional(timestamp)
msg = "A data e hora para o timestamp {timestamp} é: {datahora}"
print(msg.format(timestamp=timestamp, datahora=datahora))
E precisamos alterar também o nosso setup.py, mais uma vez. Aqui a versão alterada do setup.py:
#-*- coding: utf-8 -*-
from setuptools import setup
setup(name='meusuperprograma', # aqui o nome do seu programa
version='0.3', # temos que alterar a versão.
author='Eu Mesmo',
author_email='me@myplace.net',
url='http://meusuperprograma.org',
# ao invés de usarmos o parâmetro py_modules usamos
# o parâmetro packages.
packages=['meusuperprograma'],
# aqui indicamos onde ficam os scripts que serão instalados
scripts=['scripts/meusuperprograma'],
# Vamos colocar também alguns classificadores. Estes classificadores
# não são obrigatórios, mas deus gosta mais de você quando você
# classifica seus programas.
# Você pode ver uma lista com todos os classificadores aqui:
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Natural Language :: Portuguese',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Topic :: Software Development :: Libraries :: Python Modules',
],
)
E vamos fazer o release de novo e depois testar.
$ cd ~/dist-teste
$ python setup.py sdist upload -r testpypi
running sdist
running egg_info
[ output cortado ]
running check
[ output cortado ]
creating dist
Creating tar archive
removing 'meusuperprograma-0.3' (and everything under it)
running upload
Submitting dist/meusuperprograma-0.3.tar.gz to https://testpypi.python.org/pypi
Server response (200): OK
$ workon meusuperprogramaenv
$ cd ~/dir-limpo
$ pip install meusuperprograma --index-url=https://testpypi.python.org/pypi --upgrade
Downloading/unpacking meusuperprograma
[ output cortado ]
Successfully installed meusuperprograma
Cleaning up...
Agora, depois de instalado, só testar nosso programa pela linha de comando
$ meusuperprograma
A data e hora para o timestamp 1417404128.7673662 é: 01:22:08 - 01/12/2014
$ meusuperprograma 0
A data e hora para o timestamp 0.0 é: 21:00:00 - 31/12/1969
$ meusuperprograma -62135585612
A data e hora para o timestamp -62135585612.0 é: 00:00:00 - 01/01/1
É isso aí, tudo certinho.
Um programa com dependências
O nosso programa ficou tão legal que vamos até fazer uma versão web pra ele. E claro que a gente não vai fazer tudo na mão, vamos usar um framework, no caso o flask. Pra instalar é fácil, um simples pip install:
$ pip install flask
Com o flask instalado vamos criar um módulo para a nossa aplicação web e um script para rodar esta aplicação.
Primeiro, o arquivo meusuperprograma/webapp.py com a aplicação flask.
# -*- coding: utf-8 -*-
# arquivo meusuperprograma/webapp.py
from flask import Flask, Response
from meusuperprograma import faz_algo_sensacionalmente_dahora
minhasuperapp = Flask('meusuperprograma.webapp')
@minhasuperapp.route('/')
def index():
info = faz_algo_sensacionalmente_dahora()
ret = """
A data e hora atual é: {datahora}.<br/>
O timestamp pra isso é: {timestamp}
"""
return(Response(ret.format(datahora=info['datahora'],
timestamp=info['timestamp'])))
Agora o arquivo scripts/meusuperprogramaweb, que é o script para rodar nossa aplicação flask.
#!/usr/bin/env python
#-*- coding: utf-8 -*-
from meusuperprograma.webapp import minhasuperapp
if __name__ == '__main__':
minhasuperapp.run()
Com estes novos arquivos, a estrutura de diretórios ficou assim:
/home/juca/dist-teste
|-- COPYING
|-- meusuperprograma
| |-- __init__.py
| |-- modulo_a.py
| |-- modulo_b.py
| `-- webapp.py
|-- README
|-- scripts
| |-- meusuperprograma
| `-- meusuperprogramaweb
`-- setup.py
E agora vamos novamente alterar o setup.py:
#-*- coding: utf-8 -*-
from setuptools import setup
setup(name='meusuperprograma', # aqui o nome do seu programa
version='0.4', # temos que alterar a versão.
author='Eu Mesmo',
author_email='me@myplace.net',
url='http://meusuperprograma.org',
# ao invés de usarmos o parâmetro py_modules usamos
# o parâmetro packages.
packages=['meusuperprograma'],
# aqui indicamos onde ficam os scripts que serão instalados
scripts=['scripts/meusuperprograma', 'scripts/meusuperprogramaweb'],
# aqui indicamos quais as dependências de instalação
install_requires=['flask'],
# Vamos colocar também alguns classificadores. Estes classificadores
# não são obrigatórios, mas deus gosta mais de você quando você
# classifica seus programas.
# Você pode ver uma lista com todos os classificadores aqui:
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Natural Language :: Portuguese',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Topic :: Software Development :: Libraries :: Python Modules',
],
)
E é isso, tudo pronto pra lançar e testar novamente. Perceba que na hora de instalar a nova versão de meusuperprograma vamos usar o parâmetro –extra-index-url ao invés do parâmetro –index-url, isto porque queremos que primeiro seja buscado no cheese shop live e depois no de teste.
$ python setup.py sdist upload -r testpypi
running sdist
running egg_info
[ output cortado ]
running check
[ output cortado ]
Creating tar archive
removing 'meusuperprograma-0.4' (and everything under it)
running upload
Submitting dist/meusuperprograma-0.4.tar.gz to https://testpypi.python.org/pypi
Server response (200): OK
$ cd ~/dir-limpo
$ pip install meusuperprograma --extra-index-url=https://testpypi.python.org/pypi --upgrade
[ output cortado ]
Successfully installed meusuperprograma
Cleaning up...
Perceba que na hora da instalação foi instalado também, automaticamente, o flask e suas dependências.
Agora, vamos testar nossa aplicação web.
$ meusuperprogramaweb
* Running on http://127.0.0.1:5000/
E abra seu browser e acesse http://127.0.0.1:5000/ para ver a nossa aplicação web rodando.
Só que a aparência da aplicação web ficou meio xoxa, não? Vamos fazer um template lindão pra melhorar as coisas
Um programa com package data
Os arquivos que não são arquivos python, mas que serão incluídos na distribuição são chamados de package data, isso inclui o template (um arquivo .html) que usaremos para a nossa aplicação web.
Então, primeiro fazemos um template bem bonitão, que vai ficar em meusuperprograma/templates/template.html
<html>
<head>
<title>Meu Super Programa Versão Web!</title>
</head>
<body>
<div> A data e hora atual é: <span>{{ datahora }}</span></div>
<div> O timestamp pra isso é: <span>{{ timestamp }}</span></div>
</body>
</html>
E depois alteramos a nossa webapp pra que passe a usar o template:
# -*- coding: utf-8 -*-
from flask import Flask, render_template
from meusuperprograma import faz_algo_sensacionalmente_dahora
minhasuperapp = Flask('meusuperprograma.webapp')
@minhasuperapp.route('/')
def index():
info = faz_algo_sensacionalmente_dahora()
contexto = {'datahora': info['datahora'],
'timestamp': info['timestamp']}
return render_template('template.html', **contexto)
Já alteramos tudo o que precisávamos no nosso código, mas ainda precisamos alterar o setup.py e criar mais um novo arquivo, que se chamará MANIFEST.in e ficará na raiz do nosso projeto. Este arquivo MANIFEST.in é um arquivo onde dizemos quais arquivos de package data devem ser incluídos na distribuição. O nosso ficará assim:
include meusuperprograma/templates/template.html
Simplesmente usamos a diretiva include para dizer qual arquivo deve ser incluído na distribuição.
Com este novo arquivo, nossa estrutura de diretórios ficou assim:
/home/juca/dist-teste
|-- COPYING
|-- MANIFEST.in
|-- meusuperprograma
| |-- __init__.py
| |-- modulo_a.py
| |-- modulo_b.py
| |-- templates
| | `-- template.html
| `-- webapp.py
|-- README
|-- scripts
| |-- meusuperprograma
| `-- meusuperprogramaweb
`-- setup.py
Agora vamos alterar o setup.py. É uma alteração simples. Passaremos a usar o parâmetro include_package_data=True para indicar que os arquivos não-python devem ser incluídos. Com esta mudança nosso setup.py ficou assim:
#-*- coding: utf-8 -*-
from setuptools import setup
setup(name='meusuperprograma', # aqui o nome do seu programa
version='0.4', # temos que alterar a versão.
author='Eu Mesmo',
author_email='me@myplace.net',
url='http://meusuperprograma.org',
# ao invés de usarmos o parâmetro py_modules usamos
# o parâmetro packages.
packages=['meusuperprograma'],
# aqui indicamos onde ficam os scripts que serão instalados
scripts=['scripts/meusuperprograma', 'scripts/meusuperprogramaweb'],
# aqui indicamos quais as dependências de instalação
install_requires=['flask'],
# aqui dizemos que é para incluir os arquivos que não são
# arquivos python
include_package_data=True,
# Vamos colocar também alguns classificadores. Estes classificadores
# não são obrigatórios, mas deus gosta mais de você quando você
# classifica seus programas.
# Você pode ver uma lista com todos os classificadores aqui:
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Natural Language :: Portuguese',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Topic :: Software Development :: Libraries :: Python Modules',
],
)
E agora sim temos tudo pronto. Vamos gerar nossa distribuição e testar.
$ python setup.py sdist upload -r testpypi
running sdist
running egg_info
[output cortado]
running check
[output cortado]
Creating tar archive
removing 'meusuperprograma-0.5' (and everything under it)
running upload
Submitting dist/meusuperprograma-0.5.tar.gz to https://testpypi.python.org/pypi
Server response (200): OK
$ workon meusuperprogramaenv
$ cd ../dir-limpo
$ pip install meusuperprograma --extra-index-url=https://testpypi.python.org/pypi --upgrade
[output cortado]
$ meusuperprogramaweb
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
E é isso. Agora só abrir seu navegador em 127.0.0.1:5000 que você vai ser seu super programa versão web agora com um lindo template.
Tá vendo, agora não tem mais mistério em como distribuir seus projetos (puro) python. Molezinha!
Dúvidas? Fiquem à vontade, podem mandar bala!
Sumarização automática de textos na prática: Extração baseada em grafos é o que há!
- publicado em 10 de novembro de 2014
Boas, pessoal. Mais uma vez voltando dos mortos, agora vou falar um pouco sobre sumarização automática de textos usando um método estatístico.
Começando
Bom, sumarização de textos é isso mesmo que você entendeu. Tem um texto grande e a ideia é criar um texto menor, mantendo o importante da informação. Agora só falta dizer que raios é “extração” e, pior ainda, que maldito grafo é esse.
Tem um montão de jeitos de se resumir (sumarizar) um texto, sendo que os mais usados atualmente são os que baseados em análises estatísticas criam um sumário extraindo as frases principais dos texto-fonte [1]. Dentre estes, segundo [2] e [3], os que se saem melhor são os baseados em aprendizagem de máquina, isto é , que usam uma massa de dados catalogada para treinamento e a avaliação dos textos é feita com base nos resultados deste treinamento, e os baseados em grafos, que criam grafos à partir do texto-fonte e fazem a análise baseada nos nós e arestas [1] [2].
Agora que a gente já sabe o que é extração e sabe que o grafo é o que será construído com base no texto para, com base nele, decidirmos que frase extrair, vamos olhar mais de perto como funciona essa tal de extração de sentenças baseada em grafos [3].
Textos como grafos
A primeira coisa a se fazer quando trabalhando com grafos é identificar quais unidades de texto iremos usar como os nós do grafo e quais as relações entre estas unidades usaremos para criar as arestas. As características dos nós e arestas serão diferentes de aplicação para aplicação, mas independente das características dos nós e arestas, a aplicação de grafos a textos de linguagem natural consiste, de modo geral, nos seguintes passos [5]:
Identificar as unidades de texto (tokens) que melhor se encaixam no trabalho corrente e colocá-las como nós do grafo.
Identificar as relações que conectam estas unidades de texto e criar arestas, baseadas nestas relações, entre os nós do grafo.
Iterar o algorítimo escolhido para se pontuar os nós.
Ordenar os nós de acordo com a pontuação final. Usar esta pontuação para classificação/extração.
Um grafo para extração de sentenças
Como o nosso objetivo aqui é criar um sumarizador por extração, nossa tarefa é determinar quais são as sentenças mais relevantes do texto e depois usá-las para criar o resumo. Sendo assim, usaremos sentenças inteiras como nós e as arestas entre estes nós serão construídas baseadas na semelhança entre as sentenças. Desta maneira será criado um grafo não-orientado relacionando as sentenças entre si. É importante ressaltar também que, pelo menos no contexto da linguagem natural, é interessante além de considerar-se a quantidade de relações, considerar-se também a “força” destas relações, atribuindo algum peso a elas [4].
A semelhança entre duas sentenças pode ser determinada pelo número de tokens que se repetem nestas frases. Para evitar-se dar notas mais altas a sentenças grandes, usa-se um fator de normalização que dividade a o número de repetições de token pelo tamanho das sentenças. Formalmente, dadas duas sentenças Si e Sj com uma sentença sendo representada pelo conjunto de Ni palavras que aparecem na sentença: Si = Wi1, Wi2… Win, a semelhança entre Si e Sj é definida como [4]:

Semelhança entre sentenças
Com estas informações que temos até agora já podemos implementar um código que cria um grafo baseado em um texto, ou seja, os passos 1 e 2, mas ainda nos falta escolher um algorítimo para dar uma pontuação aos nós e, por fim, extrair as frases mais relevantes. Para facilitar a nossa implementação, em princípio vamos usar um algorítimo bem simples para pontuar as sentenças [4].
Neste algorítimo simplificado, a pontuação dos nós será simplesmente a soma do peso das relações que este nó tem com os outros, ou seja, a pontuação dos nós e a soma do peso as arestas relacionadas à ele. Formalmente, sendo Rel(Si) o conjunto nós relacionados à sentença Si e wij sendo o peso da relação entre Si e Sj, a pontuação de de um nó do grafo é definida como:

Com isso, agora já temos também estabelecido como faremos o passo 3. O passo 4 é simples e não requer maiores explicações.
Implementando o sumarizador simplificado
Nosso grafo consistirá em nós sendo as sentenças de um texto e as arestas sendo a semelhança entre as sentenças. Para a implementação, usaremos a linguagem de programação Python [6] em conjunto com algumas bibliotecas. São elas: NLTK [7] e NetworkX [8].
Então, antes de começar, vamos relembrar o que devemos fazer: Primeiro, vamos decompor o texto em sentenças e as sentenças em palavras. Depois disso feito, colocaremos as sentenças como os nós do grafo e as arestas serão feitas baseadas na semelhança entre as frases. Com o grafo já criado, daremos uma pontuação para os nós e por fim extrairemos as sentenças de maior pontuação [5].
Agora, deixa de papo e vamos pro que importa: o código!
# -*- coding: utf-8 -*-
import math
import nltk
import networkx as nx
class Texto:
def __init__(self, raw_text):
"""
``raw_text`` é o text puro a ser resumido.
"""
self.raw_text = raw_text
self._sentences = None
self._graph = None
def resumir(self):
"""
Aqui a gente extrai as frases com maior pontuação.
O tamanho do resumo será 20% do número de frases original
"""
# aqui definindo a quantidade de frases
qtd = int(len(self.sentences) * 0.2) or 1
# ordenando as frases de acordo com a pontuação
# e extraindo a quantidade desejada.
sentencas = sorted(
self.sentences, key=lambda s: s.pontuacao, reverse=True)[:qtd]
# ordenando as sentenças de acordo com a ordem no texto
# original.
ordenadas = sorted(sentencas, key=lambda s: self.sentences.index(s))
return ' '.join([s.raw_text for s in ordenadas])
@property
def sentences(self):
"""
Quebra o texto em sentenças utilizando o sentence tokenizer
padrão do nltk.
"""
if self._sentences is not None:
return self._sentences
# nltk.sent_tokenize é quem divide o texto em sentenças.
self._sentences = [Sentenca(self, s)
for s in nltk.sent_tokenize(self.raw_text)]
return self._sentences
@property
def graph(self):
"""
Aqui cria o grafo, colocando as sentenças como nós as arestas
(com peso) são criadas com base na semelhança entre sentenças.
"""
if self._graph is not None:
return self._graph
graph = nx.Graph()
# Aqui é o primeiro passo descrito acima. Estamos criando os
# nós com as unidades de texto relevantes, no nosso caso as
# sentenças.
for s in self.sentences:
graph.add_node(s)
# Aqui é o segundo passo. Criamos as arestas do grafo
# baseadas nas relações entre as unidades de texto, no nosso caso
# é a semelhança entre sentenças.
for node in graph.nodes():
for n in graph.nodes():
if node == n:
continue
semelhanca = self._calculate_similarity(node, n)
if semelhanca:
graph.add_edge(node, n, weight=semelhanca)
self._graph = graph
return self._graph
def _calculate_similarity(self, sentence1, sentence2):
"""
Implementação da fórmula de semelhança entre duas sentenças.
"""
w1, w2 = set(sentence1.palavras), set(sentence2.palavras)
# Aqui a gente vê quantas palavras que estão nas frases se
# repetem.
repeticao = len(w1.intersection(w2))
# Aqui a normalização.
semelhanca = repeticao / (math.log(len(w1)) + math.log(len(w2)))
return semelhanca
class Sentenca:
def __init__(self, texto, raw_text):
"""
O parâmetro ``texto`` é uma instância de Texto.
``raw_text`` é o texto puro da sentença.
"""
self.texto = texto
self.raw_text = raw_text
self._palavras = None
self._pontuacao = None
@property
def palavras(self):
"""
Quebrando as sentenças em palavras. As palavras
da sentença serão usadas para calcular a semelhança.
"""
if self._palavras is not None:
return self._palavras
# nltk.word_tokenize é quem divide a sentenças em palavras.
self._palavras = nltk.word_tokenize(self.raw_text)
return self._palavras
@property
def pontuacao(self):
"""
Implementação do algorítimo simplificado para pontuação
dos nós do grafo.
"""
if self._pontuacao is not None:
return self._pontuacao
# aqui a gente simplesmente soma o peso das arestas
# relacionadas a este nó.
pontuacao = 0.0
for n in self.texto.graph.neighbors(self):
pontuacao += self.texto.graph.get_edge_data(self, n)['weight']
self._pontuacao = pontuacao
return self._pontuacao
def __hash__(self):
"""
Esse hash aqui é pra funcionar como nó no grafo.
Os nós do NetworkX tem que ser 'hasheáveis'
"""
return hash(self.raw_text)
Para testar vamos resumir o seguinte texto, extraído do jornal Folha de São Paulo:
Dezenas de veículos foram incendiados em frente a sede do governo da região de Guerrero, no México, em um protesto pelo desaparecimento e morte de 43 estudantes da escola normal rural de Ayotzinapa.
Mais de 300 jovens, a maioria com o rosto coberto, atacaram a fachada do edifício em Chipancingo, capital de Guerrero.
O protesto ocorreu após o procurador-geral da República do país, Jesús Murillo Karam, informar que três homens suspeitos de ser integrantes do cartel Guerreros Unidos confessaram ter matado os estudantes e queimado seus corpos. O presidente Enrique Peña Nieto prometeu na sexta (7) punir todos os responsáveis pelos «crimes abomináveis».
Os jovens sumiram em 26 de setembro, depois de arrecadar fundos para a escola em Iguala (a 192 km da Cidade do México).
Na saída da cidade, dois ônibus que voltavam à instituição com os alunos foram alvejados por policiais e traficantes do Guerreros Unidos. O ataque deixou seis mortos.
Os confessores, identificados como Particio Reyes, Jonathan Osorio e Agustín García Reyes, dizem que receberam os 43 estudantes no lixão de Cocula, a 22 km de Iguala. Segundo os pistoleiros, 15 deles chegaram ao local mortos com sinais de asfixia.
Segundo a Procuradoria-Geral do México, os detidos não disseram quem levou os estudantes e quem era o mandante da emboscada.
O órgão, porém, acredita que o mandante foi o prefeito de Iguala, Jose Luis Abarca, preso na quarta (5). A intenção seria evitar que os alunos atrapalhassem um evento em que sua mulher, María de los Ángeles Pineda, seria lançada como candidata a sucedê-lo. A mulher de Abarca é irmã de três chefes do Guerreros Unidos.
FAMILIARES
Em entrevista, os parentes disseram não acreditar na versão do procurador-geral e pediram que o material recolhido seja analisado por peritos independentes.
Para eles, o governo quer fazer com que eles acreditem que seus filhos estão mortos. «Sequer mostraram fotos dos nossos filhos. Enquanto não houver provas, nossos filhos estão vivos», disse Felipe de la Cruz, pai de um dos alunos.
Os pais pediram ao governo que prossiga com as buscas e permita a assistência técnica da Comissão Interamericana de Direitos Humanos.
Agora, para usar o código, num shell de python, importe o módulo, crie uma instância da classe Texto e use o método resumir(), assim:
>>> import sumarizacao
>>> t = sumarizacao.Texto(txt)
>>> resumo = t.resumir()
Aqui a representação do grafo. A largura das arestas é baseada na força das relações entre as frases e o tamanho dos nós é baseado na pontuação destes e o número dentro dos nós é índice da sentença no texto.

Agora o resumo gerado pelo nosso código:
Dezenas de veículos foram incendiados em frente a sede do governo da região de Guerrero, no México, em um protesto pelo desaparecimento e morte de 43 estudantes da escola normal rural de Ayotzinapa. O protesto ocorreu após o procurador-geral da República do país, Jesús Murillo Karam, informar que três homens suspeitos de ser integrantes do cartel Guerreros Unidos confessaram ter matado os estudantes e queimado seus corpos. Segundo a Procuradoria-Geral do México, os detidos não disseram quem levou os estudantes e quem era o mandante da emboscada.
Finale
Este aqui é só um exemplo de como funciona a sumarização de texto usando grafo. Numa implementação pra valer seria melhor implementar o TextRank ou algum outro bom algorítimo, não este nosso aqui, como algorítimo de pontuação e utilizar algumas técnicas, como remoção de sufixos entre outras, para melhorar o desempenho do algorítimo. Além disso, em textos jornalísticos, temos que ter cuidado com as aspas [6] incluídas no texto, com entrevistas, com listas… Na verdade, numa implementação real há bastantes detalhes a serem levados em consideração. E tenho a impressão de que pra cada implementação, com um foco diferente, os detalhes de implementação serão diferentes também.
Mas, independentemente dos detalhes de implementação, a ideia geral de sumarização extrativa por grafos está aí. Crie um grafo com as unidades de texto que melhor representam o texto para a tarefa em questão, pontue os nós de acordo com o algorítimo escolhido e por fim extraia os nós mais bem pontuados e é isso. Molezinha, não?
Referências
[1] Martins, C.B.; Pardo, T.A.S.; Espina, A.P.; Rino, L.H.M. (2001) - Introdução à Sumarização Automática.
[2] Margarido, P.R.A.; Pardo, T.A.S.; Aluísio, S.M. (2008) - Sumarização Automática para Simplificação de Textos: Experimentos e Lições Aprendidas.
[3] Leite, D.S. & Rino, L.H.M (2006) - Uma comparação entre sistemas de sumarização automática extrativa.
[4] Mihalcea, R. (2004) - Graph-based Ranking Algorithms for Sentence Extraction, Applied to Text Summarization
[5] Mihalcea, R. & Tarau, P. (2004) – TextRank: Bringing Order into Texts
[6] Python Programming Language - https://www.python.org/
[7] Natural Language Toolkit - http://www.nltk.org/
[8] NetworkX - https://networkx.github.io/
Notas de rodapé
@python - Entendendo decorators
- publicado em 22 de dezembro de 2013
Fala, pessoal. Tranquilidade? Hoje eu vou falar sobre decorators do Python. Os decorators são aquelas coisas, começadas por uma arroba, que você usa «em cima» das suas definições de funções/métodos/classes. Algumas que você já deve ter usado são:
class Classe:
@classmethod
def método_de_classe(cls):
print('bla')
@property
def read_only(self):
return True
Eles servem pra alterar o comportamento das nossas funções/métodos/classes de diversas maneiras. Nos nossos exemplos aí, o primeiro decorator faz com que o nosso método receba a classe e não a instância como primeiro parâmetro, e no segundo, faz com que chamemos nosso método como se fosse um atributo, e se tentarmos fazer uma atribuição a isso, vai dar erro.
Deu pra perceber que dá pra fazer um monte de coisa legal com os decorators, não? E pra usar já vimos também que não é difícil, é só colocar um «@meu_decorator» em cima das nossas declarações, e já era.
Pra escrever também não é difícil, a gente só precisa entender direitinho o que é um decorator e depois fica fácil. Então, vamos lá!
O que é um decorator?
Bom, a definição mais simples de um decorator é a seguinte: Um decorator é um callable que retorna um callable. Ponto. Callable é um cara que você pode «chamar» - tipo coisa() - como uma função, um método… qualquer objeto que tenha o método __call__.
Seguindo por essa linha, podemos fazer assim, o nosso primeiro decorator:
#-*- coding: utf-8 -*-
def um_decorator(func):
"""
Um callable que tem um callable como parâmetro
e retorna um callable.
"""
# faz nada...
print('decorating func')
return func
E, no shell, usamos assim:
>>> @um_decorator
... def some_func():
... print('oi')
...
decorating func
>>> some_func()
oi
>>>
Vamos parar por aqui e entender o que aconteceu.
Entendendo o funcionamento do decorator
Assim que definimos nossa função, o interpretador “viu” que esta função estava decorada, isto é, havia algo começando por um arroba antes da definição e adicionou às variáveis globais, usando o nome da sua fução, não a função que você definiu, mas o retorno do seu decorator. Isso significa que assim que o interpretador “viu” que sua função estava decorada, ele fez algo que seria tipo isso:
>>> some_func = um_decorator(some_func)
decorating func
>>>
E aqui que está toda a jogada. Agora, quem está usando o nome “some_func” não é mais a função que você definiu, e sim a função que o nosso decorator retornou.
Pra exemplificar melhor, vamos fazer uma segunda versão do decorator:
#-*- coding: utf-8 -*-
def um_decorator(func):
"""
Um callable que tem um callable como parâmetro
e retorna um callable.
"""
def other_func():
print('ola')
return other_func
E no shell fica assim:
>>> @um_decorator
... def some_func():
... print('oi')
...
>>> some_func
<function um_decorator.<locals>.other_func at 0x7fac11ffe050>
>>> some_func()
ola
>>>
Então, o que acabamos de ver aí é que quem está usando o nome “some_func” não é a função que definimos e sim a função other_func, que definimos dentro do nosso decorator. Legal, né?
O que fizemos até aqui foi inútil, eu sei, mas vamos melhorar daqui pra frente. Prometo. :)
Um decorator melhorzinho
O que a gente vai fazer agora é o seguinte: um decorator pra logar as coisas antes e depois da execução de alguma coisa. Algo mais ou menos assim:
#-*- coding: utf-8 -*-
def loga(func):
"""
Decorator que loga a execução do callable
"""
def loga_execucao(*args, **kwargs):
print('iniciando execucao com %s, %s' % (str(args), str(kwargs)))
retorno = func(*args, **kwargs)
print('terminou execucao com %s' % retorno)
return loga_execucao
E, novamente, no shell fica assim:
>>> @loga
... def some(a, b):
... return a + b
...
>>> some(1, 1)
iniciando execucao com (1, 1), {}
terminou execucao com 2
>>> from random import random
>>> @loga
... def do_magic(*args, **kwargs):
... return random()
...
>>> do_magic(1, 'asdf', nada='não sei', acre=NotImplemented)
iniciando execucao com (1, 'asdf'), {'nada': 'não sei', 'acre': NotImplemented}
terminou execucao com 0.28372230130165577
>>>
O funcionamento básico aqui é a mesma coisa do nosso outro decorator, a função loga retornou a função loga_execucao e esta função está usando o nome da função que foi decorada. Mas, além disso, tiveram umas coisas um pouco diferentes, então vamos parar por aqui, respirar um pouco e ver tudo com calma.
Entendendo o funcionamento do decorator melhorzinho
A primeira coisa diferente que notamos agora é que a função loga_execucao tem como parâmetros *args e **kwargs (linha 8). Isso significa que vale tudo, aceita quaisquer argumentos - Nota à parte: isso, do *args e **kwargs, é MUITO da hora. Isso porque queremos decorar qualquer coisa e qualquer coisa pode ter qualquer argumento.
A outra coisa diferente é o que importa aqui. Na linha 10, a gente chama a func(), que é a função que passamos para o decorator. Apesar de a função “func “ não estar no namespace da função loga_execucao (linhas 8-11), a quando chamamos func(), ela é recuperada do namespace antecessor, isto é, da função loga (linhas 4-12). Então, quando a toda vez que a função loga_execucao for executada, o interpretador vai lembrar que no momento da criação dela, existia um parâmetro chamado func, que é a função que você passou pro decorator.
E é isso, essa é toda a mágica dos decorators. Aqui você já pode fazer muitas coisas legais com eles, mas ainda tem mais!
Um decorator com classe
Bom, agora nós vamos fazer um decorator usando uma classe, não funções. O esquema de funcionamento é o mesmo, um callable que retorna outro callable. O nosso decorator agora será um decorator para cachear funções custosas. Se uma fução demora muito pra executar, deixamos o resultado em memória e da próxima vez já pegamos o resultado computado. O decorator fica mais ou menos assim:
#-*- coding: utf-8 -*-
CACHE = {}
class cacheado:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
# cacheia a função
name = self.func.__name__
cacheado = CACHE.get(name)
if not cacheado:
cacheado = self.func(*args, **kwargs)
CACHE[name] = cacheado
return cacheado
E no shell fica assim:
>>> @cacheado
... def take_time():
... lista = []
... for i in range(100000):
... lista.insert(0, i)
... return list
...
>>> take_time
<__main__.cacheado object at 0x7f8c1fd81790>
>>> type(take_time)
<class '__main__.cacheado'>
>>>
>>> timeit.timeit(take_time, number=1)
4.054789036999864
>>> timeit.timeit(take_time, number=1)
9.35900243348442e-06
>>> timeit.timeit(take_time.__call__, number=1)
1.2202999641885981e-05
>>>
Viram? Na primeira vez levou 4 segundos. Da segunda foi instantâneo. Agora vamos entender direito o que aconteceu aí.
Entendendo o decorator com classe
Quando fazemos @cacheado, estamos fazendo algo assim, lembra?
>>> def other_take_time():
... sleep(10)
... return True
...
>>> other_take_time = cacheado(other_take_time)
>>> other_take_time
<__main__.cacheado object at 0x7f8c1eca0690>
>>>
Então, como agora nosso decorator é uma classe, a função decorada é uma instância da classe “cacheado”, e quando chamamos a função decorada, estamos na verdade chamando o método __call__ da instância de “cacheado”. Simples também, não?
Um decorator com parâmetros
Vamos fazer um outro decorator “cacheado”, mas agora aceitará como argumento quantos segundos o resultado ficará cacheado. Assim:
#-*- coding: utf-8 -*-
from time import time
CACHE = {}
class cacheado:
def __init__(self, tempo):
"""
Recebe o tempo, em segundos, que o resultado
da função ficará cacheado.
"""
self.tempo = tempo
def __call__(self, func):
# cacheia a função
# agora, __call__ será chamado pelo @ na construção
# da função, isto é, uma vez só, e sendo assim, __call__
# tem que retornar um callable. Agora, __call__
# é o nosso decorator.
def cacheia_resultado(*args, **kwargs):
name = func.__name__
agora = time()
cacheado = CACHE.get(name)
if not cacheado or ((cacheado[1] + self.tempo) < agora):
retorno = func(*args, **kwargs)
cacheado = (retorno, agora)
CACHE[name] = cacheado
retorno = cacheado[0]
return retorno
return cacheia_resultado
E usamos assim:
>>> @cacheado(10)
... def take_time():
... lista = []
... for i in range(100000):
... lista.insert(0, i)
... return lista
...
>>> take_time
<function cacheado.__call__.<locals>.cacheia_resultado at 0x7f8c1ec9f680>
>>> timeit.timeit(take_time, number=1)
4.03598285499902
>>> timeit.timeit(take_time, number=1)
0.015197464999801014
>>> # alguns segundos depois
...
>>> timeit.timeit(take_time, number=1)
4.0627139449934475
A coisa aí mudou um pouco de figura agora, vamos parar de novo pra entender.
Entendendo o decorator com parâmetro
Agora, quando fizemos @cacheado(10), fizemos algo assim:
>>> def other_take_time():
... sleep(10)
... return True
...
>>> decorator = cacheado(10)
>>> decorator
<__main__.cacheado object at 0x7f8c1eca0590>
>>> other_take_time = decorator(other_take_time)
>>> other_take_time
<function cacheado.__call__.<locals>.cacheia_resultado at 0x7f8c1ec9fd40>
>>>
Aí, o decorator não éra mais uma classe ou uma função, e sim uma instância da classe “cacheado”. Com isso, o método __call__ é chamado na criação da função decorada, e não é mais a função decorada, como no exemplo anterior. A função decorada agora é “cacheia_resultado”, a função que definimos dentro do método __call__. Tricky, mas simples, não? Pouco código pra uma coisa legal dessas… Da hora.
Estamos quase lá, mas vamos fazer mais um, só por curiosidade…
Um decorator com parâmetro opcional
Agora que a gente já sacou como funcionam os decorators, fica mole fazer um com parâmetro opcional. Aposto que você já tá pensando em como se faz. Então vamos escrever logo isso.
#-*- coding: utf-8 -*-
from time import time
CACHE = {}
class _cacheado:
def __init__(self, tempo):
"""
Recebe o tempo, em segundos, que o resultado
da função ficará cacheado.
"""
self.tempo = tempo
def __call__(self, func):
# cacheia a função
# agora, __call__ será chamado pelo @ na construção
# da função, isto é, uma vez só, e sendo assim, __call__
# tem que retornar um callable. Agora, __call__
# é o nosso decorator.
def cacheia_resultado(*args, **kwargs):
name = func.__name__
agora = time()
cacheado = CACHE.get(name)
if not cacheado or ((cacheado[1] + self.tempo) < agora):
retorno = func(*args, **kwargs)
cacheado = (retorno, agora)
CACHE[name] = cacheado
retorno = cacheado[0]
return retorno
return cacheia_resultado
def cacheado(param):
"""
param pode ser tanto um callable - no caso de o decorator
não ser usado com parâmetro ou o tempo, se usado com parâmetro.
"""
if not callable(param):
# usou o decorator com parâmetro, assim:
# cacheado(5)
decorator = _cacheado(param)
return decorator
else:
# usado sem parâmetro, usaremos o tempo padrão.
tempo = 10
decorator = _cacheado(tempo)
# Ao invés de retornar o decorator, como quando usando
# com parâmetro, temos que retornar a função decorada
# pela instância de _cacheado por que a funçaõ 'cacheada'
# já foi usada como o decorator
função_decorada = decorator(param)
return função_decorada
E usamos assim:
>>> @cacheado
... def take_time():
... lista = []
... for i in range(100000):
... lista.insert(0, i)
...
>>> timeit.timeit(take_time, number=1)
4.068460593000054
>>> timeit.timeit(take_time, number=1)
1.612800406292081e-05
>>>
>>> @cacheado(30)
... def other_take_time():
... sleep(10)
... return True
...
>>> timeit.timeit(other_take_time, number=1)
10.007121031994757
>>> timeit.timeit(other_take_time, number=1)
1.7487000150140375e-05
>>>
Belezinha, né? Acho que chegamos inteiros ao fim e deu pra sacar que não tem nada de mistério com os decorators, bem ao contrário, certo?
Ficaram dúvidas? Pode perguntar! :)
Hum… ficou grande, né? Será que alguém leu até aqui?
[juca@debianmental:~/mysrc/exemplos/decorator]$ python3
Python 3.3.3 (default, Nov 27 2013, 17:12:35)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorator import black_knight
>>> @black_knight
... def multiplica(a, b):
... return a*b
...
>>> multiplica(2, 3)
None shall pass
>>>
[juca@debianmental:~/mysrc/exemplos/decorator]$ su
Senha:
root@debianmental:/home/juca/mysrc/exemplos/decorator# python3
Python 3.3.3 (default, Nov 27 2013, 17:12:35)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from decorator import black_knight
>>> @black_knight
... def multiplica(a, b):
... return a*b
...
>>> multiplica(2, 3)
You are a looney.
6
>>>
Kudos pra quem postar o código de “black_knight”!
Criando um daemon com Python
- publicado em 03 de dezembro de 2013
Fala, pessoal, tranquilidade? Como de costume, fiquei muito tempo sem postar. Pra voltar, vamos ver como criar um daemon usando Python. Mas primeiro…
O que é um daemon?
Um daemon é um processo que fica sendo executado em segundo plano, sem contato interativo com um usuário e desassociado de um tty. O nome daemon vem do Demônio de Maxuel, e foi usado pela primeira vez (em computação, claro) pelo pessoal do projeto MAC[1]. Os daemons estão presentes no Unix desde os primórios. Aquele monte de *d que você vê, como sshd, httpd, crond e etc, são todos daemons.
Como criar um daemon?
Bom, a explicação rápida pra isso é: com o bom e velho fork-off-and-die. Cria-se um uma cópia de um processo, mata-se o processo pai e faz-se o trabalho no processo filho. A explicação longa é a seguinte:
Cria-se um fork e sai do processo pai. Com isso, libera-se o controle de shell se o daemon foi invocado de um. Também atribui-se outro id para o processo filho, fazendo com que ele não seja session leader.
Criar uma nova sessão sem um terminal de controle associado.
Criar outro fork e sair novamente do pai - aquele que foi filho antes para garantir novamente que o processo não será session leader.
Alterar a máscara de arquivos (umask).
Alterar o diretório de trabalho.
Fechar todos os descritores de arquivos descecessários.
E, agora sim executar o seu trabalho.
Por que o segundo fork()?
Bom, essa é uma discussão grande. Geralmente é dito que o segundo fork é necessário para evitar que o seu daemon obtenha um terminal de controle nos SystemV R4. Como esse já é um sistema em desuso, diz-se que o segundo fork é desnecessário. Mas a especificação POSIX diz o seguinte[2]:
The controlling terminal for a session is allocated by the session leader in an implementation-defined manner. If a session leader has no controlling terminal, and opens a terminal device file that is not already associated with a session without using the O_NOCTTY option (see open()), it is implementation-defined whether the terminal becomes the controlling terminal of the session leader
Então, como é uma questão de implementação, prefiro continuar usando um segundo fork. :) Agora, vamos parar de falação e ir pro que importa.
O código
#-*- coding: utf-8 -*-
import os
import sys
import resource
def create_daemon(stdout, stderr, working_dir):
"""
cria um daemon
"""
# faz o primeiro fork
_fork_off_and_die()
# cria uma nova sessão
os.setsid()
# faz o segundo fork
_fork_off_and_die()
# altera a máscara de arquivos
os.umask(0)
# altera o diretório de trabalho
os.chdir(working_dir)
# fecha todos os descritores de arquivos
_close_file_descriptors()
# redireciona stdout e stderr
_redirect_file_descriptors(stdout, stderr)
def _fork_off_and_die():
"""
cria um fork e sai do processo pai
"""
pid = os.fork()
# se o pid == 0, é o processo filho
# se o pid > é o processo pai
if pid != 0:
sys.exit(0)
def _close_file_descriptors():
# Fechando todos os file descriptors para evitar algum
# lock
# RLIMIT_NOFILE é o número de descritores de arquivo que
# um processo pode manter aberto
limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
for fd in range(limit):
try:
os.close(fd)
except OSError:
pass
def _redirect_file_descriptors(stdout, stderr):
"""
redireciona stdout e stderr
"""
# redirecionando stdout e stderr
for fd in sys.stdout, sys.stderr:
fd.flush()
sys.stdout= open(stdout, 'a', 1)
sys.stderr = open(stderr, 'a', 1)
def daemonize_func(func, stdout, stderr, working_dir, *args, **kwargs):
"""
executa uma função como um daemon
"""
create_daemon(stdout, stderr, working_dir)
func(*args, **kwargs)
class daemonize(object):
"""
decorator para executar uma função como daemon
"""
def __init__(self, stdout='/dev/null/', stderr='/dev/null/',
working_dir='.'):
# stdout e stderr são os lugares para onde serão redirecionados
# sys.stdout e sys.stderr
self.stdout = stdout
self.stderr = stderr
# working_dir é o diretório onde o daemon trabalhará
self.working_dir = working_dir
def __call__(self, func):
def decorated_function(*args, **kwargs):
daemonize_func(func, self.stdout, self.stderr, self.working_dir,
*args, **kwargs)
return decorated_function
E agora você pode usar esse código assim:
#-*- coding: utf-8 -*-
import time
from daemonize import daemonize
@daemonize(stdout='log_b.txt', stderr='log_b.txt')
def b():
print('comecando')
time.sleep(10)
print('fim')
if __name__ == '__main__':
b()
Ou assim:
#-*- coding: utf-8 -*-
import time
from daemonize import daemonize_func
def a():
print(time.time())
time.sleep(50)
print(time.time())
if __name__ == '__main__':
daemonize_func(a, stdout='log_a.txt', stderr='log_a.txt', working_dir='.')
É isso, pessoal. Valeu. :)
[1] http://www.takeourword.com/TOW146/page4.html
[2] http://pubs.opengroup.org/onlinepubs/007904975/basedefs/xbd_chap11.html#tag_11_01_03
Manifestações, micaretações, os manifestantes mais-amor e os nacionalistas
- publicado em 20 de julho de 2013
As manifestações contra o aumento das passagens organizadas pelo MPL começaram este ano em 06 de junho. Até o momento, dia 20 de junho, foram realizadas 6 manifestações o outra está marcada para hoje às 17:00 horas. Os quatro primeiros atos contaram com um número de participantes entre cinco mil no dia 06, e pouco mais de dez mil no dia 13. Os quinto e sexto atos, ocorridos nos dias 17 e 18 tiveram uma participação muito maior, com estimativas dando conta de mais de 100 mil pessoas. O que haveria mudado para que em quatro dias uma multidão que não está acostumada a participar de manifestações resolvesse passar a participar? Poderíamos dizer que o gigante acordou, mas isso não é explicação, é imagem da propaganda de uísque.
Os primeiros atos foram todos marcados por confrontos entre policiais e manifestantes. O ato do dia 13, já começou com um “clima” de escaramuça devido aos conflitos ocorridos no ato anterior, o maior até então, resultando em manifestantes presos e policiais feridos. As estratégias do governo do estado e da prefeitura eram a mesma, a mesma de sempre: vamos aumentar as passagens; vão reclamar; limpamos a bagunça e fim de papo. Mas no dia 13 o comando deixou a tropa solta demais, e o resultado foram mais confrontos e dessa vez com imagens muito ruins, com transeuntes e imprensa sendo feridos. Uma matilha solta e umas manchetes ruins fizeram o comando da polícia repensar suas táticas. Foram obrigados a segurar a tropa, e com isso uma nova maneira de limpar a bagunça deveria ser encontrada.
Já o Movimento Passe Livre estava numa curva crescente, com cada vez mais mobilização em suas manifestações de 2013. Com a repercussão da manifestação do dia 13, soube muito bem como canalizar a “publicidade favorável” em direção ao seu próximo ato, no dia 17, que seria o primeiro que teria uma micaretação com manifestantes mais-amor
A definição de micaretação é uma aglomeração de pessoas, que vão se manifestar politicamente, mas que no final das contas, estão mais interessadas em participar do ato em si, participar da festa, postar fotos na internet e coisas assim. O objetivo pode ser qualquer coisa: do caos instaurado no transporte coletivo à falta de amor em São Paulo passando pelo alto custo dos jogos de PS3. Na verdade, quem liga? Já o manifestante mais-amor, é o típico componente de uma micaretação. Ele fica bravo na internet, reclama dos políticos, do povo, do mundo todo e ultimamente estava meio entediado e andou vendo umas notícias de uns lugares lá longe e ficou com vontade de também ter sua própria manifestação-revolução (manifestação pacifico-revolucionaria-show-de-civilidade, claro).
Aqui entram os nacionalistas. Eles também estão buscando seus espaços e tentando espalhar suas ideias. E as mensagens deles são as mais palatáveis para o manifestante mais-amor. Eles falam sobre amor ao Brasil, volta da ordem e afins, e esse é um discurso muito difundido, o mais-amor já ouviu isso muitas vezes, quando ouve isso na sua micaretação, se encontra.
Com todos estes ingredientes, chegamos à manifestação do dia 17, onde houve a “mistura” de tudo isso: a crescente do movimento pela redução das passagens encabeçado pelo MPL, os manifestantes mais-amor empolgados com as histórias da internet e com toda a publicidade positiva veiculada nos meios de comunicação durante o fim de semana, e os nacionalistas e a direita em geral. O resultado foi uma manifestação realmente massiva, com a presença de dezenas de milhares de pessoas, mas por outro lado foi também uma manifestação desfocada, com a direita querendo impor sua própria agenda, despolitizada, com a maioria dos manifestantes sendo do tipo mais-amor, também tentando impor sua “agenda” despolitizada e com alguma ação radicalizada por parte dos manifestantes que se dirigiram ao palácio dos bandeirantes. A manifestação do dia 18 foi mais ou menos como está, mas as ações radicalizadas se dirigindo à sede da prefeitura. No dia 19, a prefeitura e o governo do estado anunciaram a revisão do aumento das tarifas do transporte nos ônibus, trens e metrô de São Paulo.
À primeira vista, temos uma vitória do movimento. E temos mesmo, a reivindicação inicial foi atendida, o preço das passagens baixou. Mas não custa olhar mais de perto. Primeiro: qual foi o papel da multidão de manifestantes mais-amor presentes nas duas últimas manifestações? Ele foi um papel decisivo, ou seja, uma participação sem a qual não haveria a conquista da reivindicação inicial? Neste ponto podemos olhar para os exemplos de outras cidades do país que tiveram o valor da tarifa do transporte reduzido por conta das manifestações. E nestas cidades não se viu toda essa “mobilização popular”, donde se conclui que este papel, seja ele qual for, não foi decisivo na obtenção do resultado obtido até agora.
Começa-se a vislumbrar o papel exercido pela massa de manifestantes mais-amor ao pensar-se em qual estratégia de contensão de danos poderia ser usada pelo governo do estado. Depois do dia 13, com a tropa na coleira, uma tática mais sutil teria que ser usada para “esvaziar” o movimento. Se você não pode bater e assustar para as pessoas não irem, nada melhor do que inflar em número e esvaziar no conteúdo político direto da manifestação. E assim foi feito, com a anuência dos meios de comunicação (desde que a classe que controla os meios de comunicação é a mesma que detém o controle do estado). Mas então, por que a tarifa foi reduzida? Porque a estratégia do governo do estado não foi tão bem sucedida. A manifestação realmente foi muito aumentada em número, na maioria dos casos, por manifestantes mais-amor, mas uma parcela dos manifestantes ainda continuou com o objetivo inicial, levando ao cabo protestos mais radicais e efetivos. Com isso, a tarifa teve que ser baixada.
Agora, sobram os questionamentos de qual será a herança e a continuação disto tudo. Aos militantes do MPL, cabe a tarefa de tentar fazer dos limões uma limonada, ou seja, conseguir canalizar pelo menos uma parte do apoio conquistado nos últimos dias para a questão central do MPL, que é o passe livre no transporte coletivo. À pessoas como eu, que estão de fora, cabe analisar como foi o funcionamento desta “manifestação espontânea” assim como analisar o papel da direita, principalmente dos nacionalistas, e em como eles tentarão captar o apoio popular.
Manifestações espontâneas, sem direção e coisas do tipo têm sua serventia e seu valor, mas também sofrem por serem muito fáceis de serem tomadas por elementos externos (no caso atual, nacionalistas e fascistas). E como os símbolos nacionalistas e as ideias fascistas são as ideias correntes, elas são muito facilmente assimiladas pelas massas. E aí reside um problema sério a ser enfrentado. A ascensão fascista na Europa no século XX teve como base, muitas vezes, o apoio popular, conseguido dessa maneira, com elementos fascistas se infiltrando em movimentos populares e cooptando os participantes menos conscienciosos com seus discursos de fácil assimilação. Percebendo-se disso, é preciso enfrentar esses problemas em todos os aspectos possíveis, desde o aspecto ideológico até o enfrentamento nas ruas e a quebra da ordem estabelecida. Os fascistas são bons nisso, os antifascistas precisam ser mais fortes e organizados que eles, ou fatalmente serão derrotados.
ATUALIZAÇÃO EM 21/06
O que era apenas uma probabilidade, agora está confirmada. Os fascistas conseguiram galgar seus espaços dentre os manifestantes mais-amor. Tanto é que a primeira linha da matilha fascista, os carecas e skin heads, estavam presentes ao ato, muito à vontade. E como esperado, os seus slogans nacionalistas caíram nas graças da massa. Agora já podemos dizer que o resquício destes atos, além da redução da tarifa dos transportes, foi, pelo menos em São Paulo, um maior contato e uma assimilação – talvez inadvertidamente – dos símbolos fascistas por parte da massa de manifestantes, os manifestantes mais-amor.
Agora o problema é muito maior, tem que se lutar para desconstruir estes símbolos fascistas no imaginário coletivo, ou o apoio aos fascistas, entre a população geral, pode aumentar. E temos exemplos de como isto termina.
Automatizando o deploy com fabric
- publicado em 08 de janeiro de 2012
Fala, pessoal. Tudo certo? Hoje eu vim falar sobre o fabric. O fabric é um cara que te ajuda a automatizar o deploy permitindo que você execute comandos de shell na máquina local e (o que é mais legal) em um servidor remoto de maneira muito simples.
Como assim?
Imagine que você tem seu programa lá, bonitão, works on my machine certified, mas você precisa por isso em algum lugar acessível ao público. Você pode muito bem gerar um .tar do seu código, copiar pro servidor, instalar… enfim, fazer tudo o que precisa na mão. Nem é complicado. Mas fazer isso é chato, e se a coisa cresce, tem sempre o risco de esquecer algo. É aí que entra o fabric! Com ele, você escreve um script (em Python) pra fazer o seu deploy.
Tá, beleza. Mas como funciona?
É bem simples. A primeira coisa a se fazer é instalar o fabric e a maneira mais fácil de fazer isso é pelo gerenciador de pacotes do seu sistema operacional (seu s.o. tem gerenciador de pacotes, não?). Depois de instalado o fabric, é só você criar um arquivo chamado fabfile.py contendo os comandos necessários ao deploy.
Pra começar, vamos fazer um “olá” com o fabric pra gente ver como funciona. O fabfile pro nosso “ola” ficou assim:
#-*- coding: utf-8 -*-
# Arquivo fabfile.py
def ola(nome):
print 'olá, ', nome
A sintaxe pra se executar o fabfile é a seguinte: fab <nome_da_funcao>:<arg1>, <arg2>, …
Então, pra executar nosso fabfile acima, executamos o seguinte comando:
$ fab ola:juca
ola, juca
Done.
Tá, entendi. Agora um exemplo decente, vai.
Agora que já vimos como usar o fabric, vamos a um exemplo real, pra gente dar uma olhada em algumas coisas interessantes da api do fabric.
A idéia aqui vai ser a seguinte: Eu tenho um repositório git na minha máquina contendo o código que eu quero subir. O procedimento pra subir é gerar um .tar contendo o código de uma named tree qualquer (um branch, uma tag, um commit…), copiar esse tar pro servidor remoto, desempacotar o tar no servidor remoto, se já tiver uma versão mais antiga instalada, desinstalar essa versão antiga, instalar a versão nova e por fim, reiniciar o web server.
Tudo muito simples, mas ficar fazendo isso é muito chato, então o fabfile abaixo resolve isso pra gente:
import os
import time
# aqui importando uns caras legais da api do fabric
# local - roda um comando de shell na máquina local
# run - roda um comando de shell no servidor remoto
# put - faz uma cópia via ssh (scp) pro servidor remoto
# env - configurações do ambiente
from fabric.api import local, run, put, env
LOCAL_SRC_PATH = '/home/juca/mysrc/sourcecode2html'
LOCAL_BUILD_PATH = '/tmp/amazon-build'
REMOTE_SRC_PATH = '/home/deployuser/src/codeprettifier'
# aqui é uma string do tipo usuario@host[:porta]
# usuario é um usuário do sistema no servidor remoto.
# É uma string como o que você passa pro ssh
env.hosts = ['deployuser@myserver']
# É essa função que vai ser chamada na execução do fabfile, algo como:
# fab deploy:master
def deploy(tree_name):
""" Executa as ações necessárias ao deploy
"""
tar_file = _package_named_tree(tree_name)
remote_tar_path, filename = _send_file(tar_file)
_unpack_code(remote_tar_path, filename)
_uninstall_last_version()
_create_link_to_lastest(remote_tar_path)
_build()
_install()
_restart_server()
def _package_named_tree(tree_name):
""" cria um arquivo .tar.bz2 baseado numa named tree do git
"""
try:
os.mkdir(LOCAL_BUILD_PATH)
except OSError:
pass
os.chdir(LOCAL_SRC_PATH)
filename = '%s/codeprettifier-%s.tar.bz2' %(LOCAL_BUILD_PATH,
tree_name)
pack_command = 'git archive %s --prefix=codeprettifier/ |' % tree_name
pack_command += ' bzip2 > %s' % filename
# aqui, executando o comando na máquina local
# com o local() da api do fabric
local(pack_command)
return filename
def _send_file(filename):
""" send file to remote server
"""
remote_tar_path = REMOTE_SRC_PATH + '/%s/' % int(time.time())
try:
# Aqui executando run(). O mkdir aí em baixo vai ser
# executado no servidor remoto.
# Assim que o primeiro run() é chamado, vai ser perguntada
# a senha do usuário no host remoto.
run('mkdir -p %s' % remote_tar_path)
except:
pass
# aqui enviando arquivo via scp usando o put()
# da api do fabric
put(filename, remote_tar_path)
filename = filename.split('/')[-1]
return remote_tar_path, filename
def _unpack_code(remote_tar_path, filename):
""" Desempacota o código no servidor remoto
"""
run('cd %s' % remote_tar_path)
run('tar -xjvf %s/%s -C %s' % (remote_tar_path, filename, remote_tar_path))
def _uninstall_last_version():
""" unpacks the code on remote server
"""
try:
run('cd %s/latest/codeprettifier' % REMOTE_SRC_PATH)
except:
return
uninstall_command = 'cd %s/latest/codeprettifier && ' % REMOTE_SRC_PATH
uninstall_command += "sudo make uninstall | grep -v codeprettifier/ |"
uninstall_command += 'grep -v Java/ | grep -v MultiLineStringDelimiter.pm |'
uninstall_command += "cut -d'k' -f2 | grep -i CodePrettifier |"
uninstall_command += " grep -v codeprettifier.pl |xargs sudo rm"
try:
run(uninstall_command)
except:
pass
run('rm %s/latest' % REMOTE_SRC_PATH)
def _create_link_to_lastest(remote_tar_path):
run('ln -s %s %s/latest' % (remote_tar_path, REMOTE_SRC_PATH))
def _build():
""" Cria o Makefile pra instalação
"""
remote_latest_dir = REMOTE_SRC_PATH + '/latest/codeprettifier'
run('cd %s && perl Makefile.PL' % remote_latest_dir)
def _install():
""" Faz a instalação em si
"""
remote_latest_dir = REMOTE_SRC_PATH + '/latest/codeprettifier'
run('cd %s && sudo make install' % remote_latest_dir)
def _restart_server():
""" reinicia o server
"""
run('sudo /sbin/service httpd restart')
Agora, é só executar
$ fab deploy:master
[deployuser@myserver] Executing task 'deploy'
[localhost] local: git archive master --prefix=codeprettifier/ | bzip2 > /tmp/amazon-build/codeprettifier-master.tar.bz2
[deployuser@myserver] run: mkdir -p /home/deployer/src/codeprettifier/1326011668/
[deployuser@myserver] Login password:
[deployuser@myserver] put: /tmp/amazon-build/codeprettifier-master.tar.bz2 -> /home/deployer/src/codeprettifier/1326011668/codeprettifier-master.tar.bz2
[deployuser@myserver] run: cd /home/deployuser/src/codeprettifier/1326011668/
[deployuser@myserver] run: tar -xjvf /home/deployuser/src/codeprettifier/1326011668//codeprettifier-master.tar.bz2 -C /home/deployuser/src/codeprettifier/1326011668/
...
[deployuser@myserver] run: cd /home/deployuser/src/codeprettifier/latest/codeprettifier && sudo make uninstall | grep -v codeprettifier/ |grep -v Java/ | grep -v MultiLineStringDelimiter.pm |cut -d'k' -f2 | grep -i CodePrettifier | grep -v codeprettifier.pl |xargs sudo rm
[deployuser@myserver] run: rm /home/deployuser/src/codeprettifier/latest
[deployuser@myserver] run: ln -s /home/deployuser/src/codeprettifier/1326011668/ /home/deployuser/src/codeprettifier/latest
[deployuser@myserver] run: cd /home/deployuser/src/codeprettifier/latest/codeprettifier && perl Makefile.PL
...
[deployuser@myserver] run: cd /home/deployuser/src/codeprettifier/latest/codeprettifier && sudo make install
...
[deployuser@myserver] run: sudo /sbin/service httpd restart
...
Done.
Disconnecting from deployuser@myserver... done.
E pronto, seu deploy foi feito automaticamente!
Pra finalizar, quero dizer que isso foi só um exemplo, você pode escrever o procedimento de deploy que quiser com o fabric. Ele é bem versátil!
Bom, é isso pessoal. Até a próxima! :)