Conversor Python de Extratos da Caixa Econômica para o Moneylog

Não tenho mantido muito controle das minhas contas mas resolvi tentar me organizar.

O Aurélio lançou a nova versão do Moneylog e eu resolvi testar. Estou achando muito interessante.

O esquema de tags é ótimo para ver gastos específicos. Tem busca, expressões regulares, relatórios. O falecido Microsoft Money deve estar com vergonha.

Para facilitar as coisas, escrevi um conversor de extratos da Caixa Econômica e resolvi publicar, pois pode ser útil para mais alguém.

Por simplicidade, a saída vai para o console, então é só direcionar para um arquivo de saída:

python caixalog.py caixa-2009.02-txt > moneylog-2009-02.txt

Vou passar o arquivo para o Aurélio. Se for útil para você, ou se tiver alguma sugestão, deixe um comentário aqui!

Como o WordPress não permite a publicação de arquivos que não sejam de vídeo, imagem ou som, vou colocar o código aqui mesmo. Para usar é preciso colar em um arquivo “caixalog.py” e ter o python instalado.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
caixalog.py
===========

Conversor de extratos em texto da Caixa Econômica para o formato do
moneylog (http://aurelio.net/moneylog).

Autor: Enrico Batista da Luz 
Blog: https://ricobl.wordpress.com/
Criado: 2009-08-15

Exemplo de formato de entrada exportado do site da Caixa:

    "Conta";"Data_Mov";"Nr_Doc";"Historico";"Valor";"Deb_Cred"
    "0000000000000000";"20090105";"000000";"CP MAESTRO";"15.00";"D"

Uso:

    python caixalog.py arq1.txt [arq2.txt, arq3.txt] > arq_saida.txt

Exemplos:
    
    # Saída no console
    python caixalog.py extrato-2009-fev.txt
    # Um arquivo apenas
    python caixalog.py extrato-2009-fev.txt > moneylog-2009-fev.txt
    # Vários arquivos
    python caixalog.py extrato-2009-*.txt > moneylog-2009.txt

* ATENÇÃO:

    O segundo e terceiro comandos acima irão substituir os arquivos
    de saída, é recomendável fazer um backup dos seus arquivos antes
    de rodar o script.
"""

import csv
import os
import sys

# Formato das linhas para o arquivo de saída:
# DATASINALVALORTAGS E DESCRIÇÃO
output_format = '%(Data_Mov)s\t%(Deb_Cred)s%(Valor)s\t%(Historico)s'

# Mapeamento de descrição para tags padrão
tag_map = {
    'PAG BLOQTO': 'boleto',
    'CP MAESTRO': 'debito',
    'CVPREV': 'previd,invest',
    'PAG FONE': 'cel,boleto',
    'SAQ LOTER': 'saque,lot',
    'CAIXA24H': 'saque,atm',
    'TRX CT INV': 'invest',
    'CP PREPAGO': 'cel,recarga',
    'SAQ CARTAO': 'saque,caixa',
}

def add_tags(desc):
    """
    Adiciona tags à descrição se houver um mapeamento.
    """
    tags = tag_map.get(desc, None)
    if tags:
        return '%s | %s' % (tags, desc)
    return desc

def format_date(d):
    """
    Converte a data no formato yyyymmdd para yyyy-mm-dd.
    """
    return '-'.join((d[:4], d[4:6], d[6:]))

def process_file(file_path):
    """
    Processa um extrato da Caixa e converte para o formato do Moneylog.
    """
    # Verifica se o arquivo existe
    if not os.path.isfile(file_path):
        raise SystemExit('Arquivo %s não encontrado!' % file_path)

    # Abre o arquivo para leitura e cria o leitor de CSV
    input = open(input_file, 'rb')
    reader = csv.reader(input, delimiter=';')
    # Lê a primeira linha com os nomes das colunas
    headers = reader.next()
    # Processa cada linha, criando um dicionário dos valores
    # de acordo com as nomes das colunas
    for row in reader:
        # Cria o dicionário
        data = dict(zip(headers, row))
        # Formata a data
        data['Data_Mov'] = format_date(data['Data_Mov'])
        # Ajusta o sinal (vazio para crédito ou '-' para débito)
        data['Deb_Cred'] = (data['Deb_Cred'] == 'D') and '-' or ''
        # Troca pontos por vírgulas no valor
        data['Valor'] = data['Valor'].replace('.', ',')
        # Adiciona tags ao histórico
        data['Historico'] = add_tags(data['Historico'])

        # Imprime a linha formatada
        print output_format % data

    # Fecha o arquivo de entrada
    input.close()

if __name__ == '__main__':
    # Captura os parâmetros da linha de comando.
    # O primeiro é o próprio script, os seguintes
    # são os arquivos a serem processados.
    input_files = sys.argv[1:]

    # Verifica se algum arquivo foi passado
    if len(input_files) < 1:
        raise SystemExit('Parâmetros inválidos')

    # Processa cada arquivo
    for input_file in input_files:
        process_file(input_file)

Manager para diferentes conexões de banco no Django

Pra tirar um pouco as teias de aranha do blog, vou escrever sobre algo que fiz recentemente e achei bacana.

Eu estava trabalhando em um novo projeto (em Django, é claro), vinculado a um projeto principal e comecei a me sentir um pouco amarrado. Os dois projetos compartilhavam as mesmas aplicações mas depois de algumas definições no meio do caminho pode-se ver que eles precisariam compartilhar pouquíssimo conteúdo.

Há algum tempo, havia visto um manager que era capaz de se conectar a um banco diferente do padrão e achei que esta seria a solução ideal. Eu poderia separar o conteúdo dos dois projetos e, quando precisasse, usaria o banco de dados do outro projeto.

O manager que eu havia visto estava em algum destes projetos em Django que foram abertos ao público recentemente, mas não consegui mais encontrar. Então me vi obrigado a fazer um pouco de pesquisa e inventar o meu próprio.

Acabei encontrando uma solução do Eric Florenzano e outra do Kenneth Falck.

A do Erick parecia um tanto antiga e junto com a pesquisa esbarrei na changeset 10026, em que foi alterada a maneira de se conectar ao banco, ao invés de usar o módulo django.conf.settings o parâmetro passa a ser um dicionário com dados de conexão, e isso facilitou bastante.

Chega de papo furado e vamos ao código. Vou explicando por partes.

Primeiro foi preciso definir uma maneira fácil de usar conexões diferentes sem ficar repetindo no código as configurações de cada conexão. A solução copiada do Eric foi criar um dicionário de conexões nas configurações do projeto:

# Configurações padrão do banco
DATABASE_ENGINE = 'postgresql_psycopg2'
DATABASE_NAME = 'default_db_name'
DATABASE_USER = 'user'
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

# Configuração padrão + alternativas
DATABASES = {
    'default': {},
    'alternative': {
        'DATABASE_NAME': 'alternative_db_name',
    },
}

Como eu disse, quiz deixar fácil o uso de conexões diferentes, então fiz com que não fosse necessário repetir os dados de configuração de cada conexão, visto que é comum que se use o mesmo usuário, senha, engine, etc. Então se você pretende somente usar outro banco, basta mudar o DATABASE_NAME. Abaixo o código responsável por isto:

from django.conf import settings
from django.db import models, backend

# Dicionário de conexão padrão
db_settings = {
    'DATABASE_HOST': settings.DATABASE_HOST,
    'DATABASE_NAME': settings.DATABASE_NAME,
    'DATABASE_OPTIONS': settings.DATABASE_OPTIONS,
    'DATABASE_PASSWORD': settings.DATABASE_PASSWORD,
    'DATABASE_PORT': settings.DATABASE_PORT,
    'DATABASE_USER': settings.DATABASE_USER,
    'TIME_ZONE': settings.TIME_ZONE,
}

def prepare_db_settings(db_profile_name):
    """
    Recebe um dicionário de conexão alternativa e retorna um novo dicionário,
    completando propriedades ausentes com os valores padrão.
    """
    return dict(db_settings, **settings.DATABASES[db_profile_name])

Por fim, o manager se comporta normalmente, se você quiser conectar a um banco diferente é só usar o método use, informando o nome do perfil de conexão que deseja utilizar. O manager retorna a query padrão mas aponta para outro banco de dados. Bacana não?

class MultiDBManager(models.Manager):
    """
    A manager that can connect to different databases.
    """
    def use(self, db_profile_name):
    	"""
    	Return a queryset connected to a custom database.
    	"""
        # Get customized database settings to use in a new connection wrapper
        db_settings = prepare_db_settings(db_profile_name)

    	# Get the queryset and replace its connection
        qs = self.get_query_set()
        qs.query.connection = backend.DatabaseWrapper(db_settings)
        return qs

Mas nem tudo são flores, é tudo muito experimental e não foi testado a fundo. Aqui vão alguns problemas conhecidos:

  1. Você vai encontrar erros se tentar acessar models relacionados, o manager inicial vai usar a conexão alternativa, mas o model relacionado vai ser procurado no banco padrão. Para contornar isto use o método select_related(‘model_relacionado’), desta forma todos os models serão listados em apenas uma consulta ao mesmo banco.
  2. Não foi testado nenhum tipo de alteração de dados (inclusão, edição, exclusão). O post do Eric talvez possa ajudar a encontrar uma solução neste sentido.

De qualquer forma, tem sido bastante útil para integrar projetos sem fazer muita bagunça.

E você? O que achou? Deixe sua opinião nos comentários!

* Disponibilizei o código no Django Snippets também: Manager for multiple database connections.