Empacotamento e distribuição de projetos Python sem mistério - Adendo

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

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 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 1.

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.html2

<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!

Notas

1 Até a versão 3.3 era necessário colocar-se um arquivo chamado __init__.py dentro de um diretório para ser um package válido, a partir desta versão este arquivo não é mais obrigatório.

2  Esta localização é uma convenção do flask, assim nosso template será achado automaticamente.

Sumarização automática de textos na prática: Extração baseada em grafos é o que há!

Boas, pessoal. Mais uma vez voltando dos mortos, agora vou falar um pouco sobre sumarização automática de textos.

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]:

1) Identificar as unidades de texto (tokens) que melhor se encaixam no trabalho corrente e colocá-las como nós do grafo.

2) Identificar as relações que conectam estas unidades de texto e criar arestas, baseadas nestas relações, entre os nós do grafo.

3) Iterar o algorítimo escolhido para se pontuar os nós.

4) 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:

Pontuação dos nós do grafo

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 juntamente com o resumo gerado pelo nosso código. 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. 

Grafo com as semelhanças entre as sentenças.
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 aspas6 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?

Notas

1 Os métodos melhores avaliados foram o SuPor-2 [3] e o TextRank [4].

2 Apesar de ser um algorítimo multi-idioma, o TextRank alcança seus melhores resultados quando utilizadas algumas técnicas de refinamento específicas para um idioma (stemmerização, stopwords e outros)[2][3].

3 A escolha de um método baseado em grafos se deve principalmente à facilidade de implementação, já que estes métodos dependem somente da análise do texto em questão

4 A simplificação feita aqui em relação ao TextRank é que o nosso algorítimo leva em consideração somente as relações, contrariamente ao TextRank que também leva em consideração, além das relações, a pontuação dos nós com que estas relações são construídas. Pode-se imaginar um algorítimo deste tipo como sendo uma 'recomendação' de nós, um nó 'recomenda' o outro. O nosso algorítimo simples leva em consideração a quantidade e a 'força' das recomendações, o TextRank, além disso, leva em conta também quem está recomendando.

5 Aqui há dois problemas que geralmente passam ao largo nas descrições dos algorítimos de extração: temos que decidir o tamanho do resumo e temos que, depois de extrair as sentenças de acordo com a pontuação, re-ordenar as frases de acordo com a ordem no texto-fonte. Re-ordenar as sentenças é trivial, e para o tamanho do resumo usaremos um tamanho de 20% o número de sentenças do texto original.

6 As aspas são citações da fala de alguém. É comum termos aspas que contém mais de uma sentença e não é bom cortas as aspas em sentenças, sob o risco de alterar completamente o sentido da frase proferida.

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/

@python - Entendendo decorators

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:

1#-*- coding: utf-8 -*-
2
3
4def loga(func):
5    """
6    Decorator que loga a execução do callable
7    """
8    def loga_execucao(*args, **kwargs):
9        print('iniciando execucao com %s, %s' % (str(args), str(kwargs)))
10        retorno = func(*args, **kwargs)
11        print('terminou execucao com %s' % retorno)
12    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

Fala, pessoal, tranquilidade? Como de costume, fiquei muito tempo sem postar. Pre 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:

  1. 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.
  2. Criar uma nova sessão sem um terminal de controle associado.
  3. Criar outro fork e sair novamente do pai - aquele que foi filho antes para garantir novamente que o processo não será session leader.
  4. Alterar a máscara de arquivos (umask).
  5. Alterar o diretório de trabalho.
  6. Fechar todos os descritores de arquivos descecessários.
  7. 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

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

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! :)

Voltando a falar no git

Boas pessoal, essa aqui é bem rápida:
Acabei de reler um post que escrevi a um tempo atrás. O post fala como usar o git com http. O negócio é zuado. Funciona, mas não é o jeito certo de fazer a coisa.

O melhor é usar o gitorious, e você ainda pode baixar o código e ter a sua própria instalação.

Simples assim.

Poo no Perl!

Boas pessoal. Tô de volta. :)

Depois de um bom tempo sem postar nada, vou aproveitar minha semaninha de folga pra tirar as teias do porão e vamos falar um pouquinho sobre orientação a objetos no Perl.

Mas o Perl é um cara sem classe, não?

No Perl realmente não existe uma palavra mágica class ou algo do tipo, mas a gente pode criar classes sim!

As classes no Perl são criadas o mecanismo de package que também é usado mais usualmente para criar módulos tradicionais. E os objetos... bom, os objetos são referências abençoadas ! :P

Referência abençoada? Que diabo é isso?

Calma... nada de misticismos. Uma referência abençoada é só uma referência que foi passada como argumento para a função bless. É essa função a responsável por dizer: 'essa referência pertence a este package (classe)'. Depois de abençoada, você será capaz de chamar funções através dessa referência, ou, em outras palavras, você terá métodos!

Entendi, mas já cansei de papo...

Beleza, já falei bastente, e agora é hora do código.

Pra este exemplo vou criar uma classe (package) Pessoa (como um módulo normal do Perl, isso será gravado em um arquivo chamado Pessoa.pm), e nessa classe criarei o contrutor da classe e os seus outros métodos. Agora sim, o código:


1package Pessoa; # Arquivo Pessoa.pm
2
3use strict;
4use warnings;
5
6# Aqui é o contrutor da classe.
7# O nome 'new' não é uma obrigação, só
8# uma convenção de uso.
9sub new{
10 # classe onde o a referência será abençoada.
11 # Isso aqui não precisa ser passado como parâmetro
12 # na hora de instanciar o objeto, é passado implicitamente
13 my $class = shift;
14
15 # Nossa referência que será abençoada.
16 # Pode ser uma referência qualquer, não necessáriamente
17 # uma referência a um hash;
18 my $self = {};
19 $self->{NOME} = undef;
20 $self->{IDADE} = undef;
21
22 # Agora a mágica acontece
23 bless($self, $class);
24
25 return $self;
26}
27
28
29# Métodos pra proteger o acesso aos nossos atributos
30sub nome{
31 # Como $class no contrutor, $self aqui também é parâmetro implícito
32 my $self = shift;
33 $self->{NOME} = shift if @_;
34 return $self->{NOME};
35}
36
37sub idade{
38 my $self = shift;
39 $self->{IDADE} = shift if @_;
40 return $self->{IDADE};
41}
42
43# Outro metodozinho só de exemplo
44sub fale{
45 my $self = shift;
46 my $fala = shift || "Qualquer coisa";
47 return $fala
48}
49
50# Precisa retornar um valor verdadeiro
511;


 

Agora, vamos criar uma subclasse de Pessoa


1package PessoaMuda; # Arquivo PessoaMuda.pm
2
3# Importando a super classe
4use Pessoa;
5
6# Aqui estamos dizendo que PessoaMuda é uma Pessoa
7@ISA = (Pessoa);
8
9# Sobrescrevendo o método fale pra que retorne nada...
10sub fale{
11 my $self = shift;
12 return
13}
14
151;


 

E, por fim, vamos usar nossos objetos


1#!/usr/bin/env perl
2
3use strict;
4use warnings;
5
6# Importando nossas classes
7use Pessoa;
8use PessoaMuda;
9
10# Nova instância de Pessoa;
11my $p = Pessoa->new();
12$p->nome('Alguém');
13print $p->nome();
14
15$p->idade(10000);
16print $p->idade();
17
18print $p->fale('Eu nasci a 10000 anos atrás...') . "\n";
19
20# Uma pessoa muda
21my $pm = PessoaMuda->new();
22$pm->nome('Ninguém');
23print $pm->nome();
24
25$pm->idade(10000);
26print $pm->idade();
27
28# Mudos não falam, lembra? Sobrescrevemos o método...
29print $pm->fale('Alguma coisa');

 

Bom, é isso! Pra mais sobre orientação a objetos no Perl, leiam o perltoot.

Valeu, e até a próxima!

Dissecando um inseto (ou como usar o pdb, the Python Debugger)

Boas, pessoal!

Hoje eu estou aqui pra mostrar como se usa o (básico do) Python Debugger, o pdb, que é (óbvio) um depurador pra se usar com o Python.
Este exemplo é feito usando o Python 3.


Mas... como é?


Bom, existe mais de uma maneira de usar o pdb. A que vou mostrar é chamando o pdb como um script para depurar outro script.

Então, primeiro, vamos criar um scriptosco com um erro mais tosco ainda pra gente começar a brincar com o pdb. O scriptosco é o seguinte:

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

def divide(a, b):
erro = False
try:
return float(a) / float(b)
except:
erro = True

if erro:
raise Exception("Não vou te dizer qual é o erro...")

a = 1
b = 2
divide(a, b)
a = 3
b = 0
divide(a, b)

Bom, rodando isso aí, a gente tem o seguinte:



$ $python3 exemplopdb.py
Traceback (most recent call last):
File "exemplopdb.py", line 18, in
divide(a, b)
File "exemplopdb.py", line 11, in divide
raise Exception("Não vou te dizer qual é o erro...")
Exception: Não vou te dizer qual é o erro...


Será que esse tal de pdb funciona mesmo?


Agora que temos um erro, podemos usar o pdb pra achar cara. A gente vai fingir que não vê nada errado até a hora certa, tá? :P
A sintexe pra se chamar o pdb é a seguinte:
python -m pdb <meu_script>
Então, ao chamar nosso doente junto com o pdb, a gente vai cair no shell do pdb. Assim:



$ python3 -m pdb exemplopdb.py
--Return--
> /media/5511fc83-ad85-484b-9b0e-0948abcb6026_/virtualpython/lib/python3.1/encodings/__init__.py(67)normalize_encoding()->'utf_32_be'
-> return ''.join(chars)
(Pdb)

Nós começaremos criando breakpoints nas linhas 11 e 18. Fazemos isto com o comando b[reak]. Depois, utilizando o comando c[ontinue], continuaremos a execução do programa até que algum breakpoint seja encontrado. E por fim, usaremos o comando list para listar um trecho de código e ver onde paramos.


(Pdb) break exemplopdb.py:11
Breakpoint 1 at /media/sda6/Projetos & afins/scripts/exemplopdb.py:11
(Pdb) b exemplopdb.py:18
Breakpoint 2 at /media/sda6/Projetos & afins/scripts/exemplopdb.py:18
(Pdb) c
> /media/sda6/Projetos & afins/scripts/exemplopdb.py(18)()
-> divide(a, b)
(Pdb) list
13 a = 1
14 b = 2
15 divide(a, b)
16 a = 3
17 b = 0
18 B-> divide(a, b)
19
[EOF]
(Pdb)

Nosso primeiro breakpoint é bem na chamada da função, então vamos "entrar na função" e acompanhar a execução. A gente faz isso com o comando s[tep]. Uma vez na função, a gente pode acompanhar a execução linha-por-linha usando o comando n[ext] e também pode imprimir os valores das variáveis com print.


(Pdb) s
--Call--
> /media/sda6/Projetos & afins/scripts/exemplopdb.py(3)divide()
-> def divide(a, b):
(Pdb) list
1 #-*- coding: utf-8 -*-
2
3 -> def divide(a, b):
4 erro = False
5 try:
6 return float(a) / float(b)
7 except:
8 erro = True
9
10 if erro:
11 B raise Exception("Não vou te dizer qual é o erro...")
(Pdb) n
> /media/sda6/Projetos & afins/scripts/exemplopdb.py(4)divide()
-> erro = False
(Pdb) n
> /media/sda6/Projetos & afins/scripts/exemplopdb.py(5)divide()
-> try:
(Pdb) n
> /media/sda6/Projetos & afins/scripts/exemplopdb.py(6)divide()
-> return float(a) / float(b)
(Pdb) print(a, b)
(3, 0)
(Pdb) n
ZeroDivisionError: 'float division'
> /media/sda6/Projetos & afins/scripts/exemplopdb.py(6)divide()
-> return float(a) / float(b)
(Pdb)

Bom, aí matamos o erro, não? Funciona mesmo!

Moral da história: As vezes o pdb pode salvar várias horas do seu dia. Faça dele um amigo. :)