Skip to content

Instantly share code, notes, and snippets.

@rodrigobaron
Last active September 8, 2017 16:25
Show Gist options
  • Save rodrigobaron/0a9f9d549a071c11dd3d8dd44259d2a2 to your computer and use it in GitHub Desktop.
Save rodrigobaron/0a9f9d549a071c11dd3d8dd44259d2a2 to your computer and use it in GitHub Desktop.
Exemplo em pt-BR de separação de responsabilidade com python

Importa as dependencias

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref

Configura o banco de dados

engine = create_engine('sqlite:///:memory:', echo=False)
Base = declarative_base()

Define os modelos

class Conta(Base):
    ''' Conta de cliente 
    
        Parametros
        ----------
        numero : Integer
            Numero da conta do cliente
        cliente : String
            Nome do cliente
        
        Atributos
        ---------
        id : Integer
            Código gerado pelo banco de dados
        operacoes : lista-relacional<OperacaoConta>
            Operações da conta
    '''
    class ContaErro(LookupError):
        ''' Exceção para quando a conta não é valida!
        '''
        pass
    
    __tablename__ = 'contas'
    
    ##
    ## Atributos da tabela contas
    ##
    id = Column(Integer, primary_key=True)
    numero = Column(Integer, nullable=False)
    cliente = Column(String, nullable=False)
    
    ##
    ## Relacionamento com a tabela operacoes_conta - muitas-para-uma
    ##
    operacoes = relationship("OperacaoConta", back_populates="conta")
    
    def __ultima_operacao(self):
        ''' Retorna a ultima operação realizada
        '''
        op = self.operacoes[-1:]
        return op[0] if len(op) == 1 else None
    
    @property
    def saldo_atual(self):
        ''' Saldo da ultima operação
        '''
        op = self.__ultima_operacao()
        return op.saldo_atual if op else 0
    
    @property
    def sequencia_atual(self):
        ''' Sequencia da ultima operação
        '''
        op = self.__ultima_operacao()
        return op.sequencia if op else 0
    
    @staticmethod
    def create(numero=None, cliente=None):
        ''' Construtor de Conta com validações dos parametros
        '''
        c = Conta(numero=numero, cliente=cliente)
        
        if not c.numero:
            raise Conta.ContaErro('A conta deve conter um numero')
        
        if not c.cliente:
            raise Conta.ContaErro('A conta deve pertencer a um cliente')
        
        return c
    
    def __repr__(self):
        ''' Representação do objeto
        '''
        return "<Conta(id='%s', numero='%s', cliente='%s')>" % (
                            self.id, self.numero, self.cliente)
class OperacaoConta(Base):
    ''' Operações que são realizadas nas contas
    
        Parametros
        ----------
        tipo : String
            caracter indicando o tipo de operação (C = Credito, D = Debito)
        para : Conta
            conta que sera afetada pela operação
        valor : Integer
            valor da operação, 100,00 = 10000
        
        Atributos
        ---------
        saldo_atual : Integer
            saldo após efetuar a operação
        operacao_referente : OperacaoConta
            indica se a operação tem vinculo com outra operação 
            ex: transferencia = faz debito na conta1 e faz credito na conta2
    '''
    __tablename__ = 'operacoes_conta'
    
    class OperacaoContaErro(LookupError):
        '''Exceção para quando a operação da conta não é valida!
        '''
        pass
    
    ##
    ## Atributos da tabela operacoes_conta
    ##
    id = Column(Integer, primary_key=True)
    tipo = Column(String, nullable=False)
    valor = Column(Integer, nullable=False)
    saldo_atual = Column(Integer)
    ##
    ## O atributo sequencia é determinado pelo valor da sequencia da
    ## ultima operação +1
    ##
    sequencia = Column(Integer, nullable=False)
    ##
    ## Relacionamento com a tabela contas - uma-para-muitas
    ##
    conta_id = Column(Integer, ForeignKey('contas.id'), nullable=False)
    conta = relationship("Conta", back_populates="operacoes")
    ##
    ## Auto referenciamento para operações que foram realizadas devido a outra operação - uma-para-uma
    ##
    operacao_referente_filho_id = Column(Integer, ForeignKey('operacoes_conta.id'))
    operacao_referente_filho = relationship("OperacaoConta", uselist=False, 
                                      backref=backref('operacao_referente_pai', remote_side=[id], uselist=False))
    ##
    ## Restrição de (conta_id|sequencia) para ajudar a previnir condicação de corrida com outra
    ## instancia concorrente. Ex: Um celery worker pode capturar a exceção gerada por _operacao_conta_sequencia_uc
    ## e coloca a requisição devolta na fila para ser reprocessado
    ##
    __table_args__ = (UniqueConstraint('conta_id', 'sequencia', name='_operacao_conta_sequencia_uc'),
                     )
    
    @staticmethod
    def __create(tipo=None, para=None, valor=None):
        ''' Builder de OperacaoConta "generico" com validações dos parametros
        '''
        if tipo not in ('D', 'C'):
            raise OperacaoConta.OperacaoContaErro('Operação invalida!')
            
        if not para:
            raise OperacaoConta.OperacaoContaErro('É necessário informar a conta!')
        
        if not valor or valor <=0:
            raise OperacaoConta.OperacaoContaErro('O valor deve ser maior que zero!')
        
        sequencia = para.sequencia_atual + 1
        return OperacaoConta(tipo=tipo, conta=para, valor=valor, sequencia=sequencia)
    
    @staticmethod
    def debito(para=None, valor=None):
        '''Builder de operação de debito, com validações especificas
        '''
        saldo_anterior = para.saldo_atual
        op = OperacaoConta.__create(tipo='D', para=para, valor=valor)
        op.saldo_atual = saldo_anterior - valor
        if op.saldo_atual < 0:
            raise OperacaoConta.OperacaoContaErro('Saldo insuficiente!')
        return op
    
    @staticmethod
    def credito(para=None, valor=None):
        '''Builder de operação de credito, com validações especificas
        '''
        saldo_anterior = para.saldo_atual
        op = OperacaoConta.__create(tipo='C', para=para, valor=valor)
        op.saldo_atual = saldo_anterior + valor
        return op
    
    @staticmethod
    def transferencia(de=None, para=None, valor=None):
        '''Builder de operações de transferencia, uma de credito e outra de debito e referencia elas entre si
            
            Retorno
            -------
            Tupla <OperacaoConta> : com duas operações referenciadas (Débito , Crédito)
        '''
        d = OperacaoConta.debito(de, valor)
        c = OperacaoConta.credito(para, valor)
        
        d.operacao_referente_filho = c
        c.operacao_referente_pai = d
        
        return d, c
    
    def __repr__(self):
        ''' Representação do objeto
        '''
        return "<OperacaoConta(id='%s', tipo='%s', valor='%s', saldo_atual='%s', conta_id='%s', operacao_referente_id='%s')>" % (
                            self.id, self.tipo, self.valor, self.saldo_atual, self.conta_id, self.operacao_referente_filho_id)

Inicializa o banco de dados e sessão

Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

Mostrando um exemplo

c1 = Conta(numero=1111, cliente='Rodrigo')
c2 = Conta(numero=2222, cliente='André')

session.add(c1)
session.add(c2)
session.flush()
opd = {'D':'Debito', 'C': 'Credito'}

print('-'*60)
print(c1.cliente, 'está com saldo atual de', c1.saldo_atual)
print(c2.cliente, 'está com saldo atual de', c2.saldo_atual)
print('*'*60)

operacoes = []
operacoes.append(OperacaoConta.credito(para=c1, valor=99999))
operacoes = operacoes + [w for w in OperacaoConta.transferencia(de=c1, para=c2, valor=10000)]
operacoes = operacoes + [w for w in OperacaoConta.transferencia(de=c1, para=c2, valor=10000)]

session.add_all(operacoes)
session.flush()
session.commit()

for w in session.query(OperacaoConta).all():
    print("*{} {} na conta de {} no valor de {}{}{}\n----> Novo saldo {}".format(w.id,
                                                    opd[w.tipo], 
                                                    w.conta.cliente, 
                                                    w.valor,
                                                    ' referente *{}'.format(w.operacao_referente_filho.id) if w.operacao_referente_filho else '',
                                                    ' referente *{}'.format(w.operacao_referente_pai.id) if w.operacao_referente_pai else '',
                                                                             w.saldo_atual))


print('*'*60)
print(c1.cliente, 'está com saldo atual de', c1.saldo_atual)
print(c2.cliente, 'está com saldo atual de', c2.saldo_atual)
print('-'*60)
------------------------------------------------------------
Rodrigo está com saldo atual de 0
André está com saldo atual de 0
************************************************************
*1 Credito na conta de Rodrigo no valor de 99999
----> Novo saldo 99999
*2 Debito na conta de Rodrigo no valor de 10000 referente *4
----> Novo saldo 89999
*3 Debito na conta de Rodrigo no valor de 10000 referente *5
----> Novo saldo 79999
*4 Credito na conta de André no valor de 10000 referente *2
----> Novo saldo 10000
*5 Credito na conta de André no valor de 10000 referente *3
----> Novo saldo 20000
************************************************************
Rodrigo está com saldo atual de 79999
André está com saldo atual de 20000
------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment