class LancamentoAddCommand(object): def __init__(self): self._conta_dao = Inject('conta dao') self._lancamento_dao = Inject('lancamento dao') def lancamento_parser_created_handler(self, lancamento_parser): parser = lancamento_parser.add_parser( 'add', aliases=['a'], help='Adiciona um novo lançamento') parser.set_defaults(event='lancamento_add_command') parser.add_argument('origem') parser.add_argument('destino') parser.add_argument('valor', type=currency.parse) parser.add_argument('--data', '-d', type=date_converter.parse, default=date.today()) parser.add_argument('--observacao', '-o', required=False) def lancamento_add_command_handler(self, args): contas_not_found = [] origem = self._conta_dao.by_name(args.origem) if not origem: contas_not_found += ['origem'] destino = self._conta_dao.by_name(args.destino) if not destino: contas_not_found += ['destino'] if len(contas_not_found) > 0: return 'conta_not_found', {'contas': contas_not_found} else: lancamento = Lancamento() lancamento.data = args.data lancamento.origem = origem lancamento.destino = destino lancamento.valor = args.valor lancamento.observacao = args.observacao self._lancamento_dao.insert(lancamento) return 'ok', {'lancamento': lancamento} def lancamento_add_command_ok_handler(self, lancamento): observacao = lancamento.observacao if lancamento.observacao else '' print('Lançamento criado: {} {} {} {} {}'.format( lancamento.data, lancamento.origem.nome, lancamento.destino.nome, lancamento.valor, observacao)) def lancamento_add_command_conta_not_found_handler(self, contas): plural_suffix = 's' if len(contas) > 0 else '' contas_str = ', '.join(contas) print('Conta{} não encontrada{0}: {}'.format(plural_suffix, contas_str))
def __init__(self): self._connection = Inject('database connection') self._conta_dao = Inject('conta dao') self._produto_dao = Inject('produto dao') self._fornecedor_dao = Inject('fornecedor dao') self._lancamento_dao = Inject('lancamento dao') self._unknown_contas = None self._unknown_produtos = None self._unknown_fornecedores = None
class ContaListCommand(object): def __init__(self): self._dao = Inject('conta dao') @staticmethod def conta_parser_created_handler(conta_parser): conta_parser.add_parser("list", aliases=['l', 'ls'], help="Lista as contas existentes").set_defaults( event='conta_list_command') def conta_list_command_handler(self, args): contas = self._dao.list() return 'ok', {'contas': contas} def conta_list_command_ok_handler(self, contas): factory = TablePrinterFactory() factory.string_column().of_attr('nome').add() factory.string_column().of_attr('descricao').title('Descrição').add() factory.string_column().of_attr('propriedades').add() printer = factory.create() printer.print(contas)
class LancamentoListCommand(object): def __init__(self): self._dao = Inject('lancamento dao') def lancamento_parser_created_handler(self, lancamento_parser): parser = lancamento_parser.add_parser( 'list', aliases=['ls'], help='Lista os lançamentos existentes') parser.set_defaults(event='lancamento_list_command') # TODO # parser.add_argument('-f', '--filter', type=str, help='Filter to be applied') # parser.add_argument('-b', '--balance', action='store_true', help='Calculate the balances for each line') def lancamento_list_command_handler(self, args): lancamentos = self._dao.list_with_contas() return 'ok', {'lancamentos': lancamentos} def lancamento_list_command_ok_handler(self, lancamentos): factory = TablePrinterFactory() factory.date_column().of_attr('data').add() factory.string_column().of_attr('origem.nome').add() factory.string_column().of_attr('destino.nome').add() factory.currency_column().of_attr('valor').add() factory.string_column().of_attr('observacao').title('Observação').add() printer = factory.create() printer.print(lancamentos)
class DatabaseDumpCommand(object): def __init__(self): self._logger = Inject('logger') self._connection = Inject('database connection') self._config = Inject('app configuration') self._git = Inject('fdc git wrapper') def database_parser_created_handler(self, db_parser): db_parser.add_parser("dump", help="Dump database.db to database.dump").set_defaults( event='database_dump_command') # TODO Flag to skip specific git steps (or all of then) def database_dump_command_handler(self, args): with open(self._config['fdc.dump_full_path'], 'w') as file: for line in self._connection.iterdump(): file.write('%s\n' % line) self._git.add(self._config['fdc.dump_full_path']) self._git.commit('--message', 'Automatic database dump') self._git.push() return 'ok' def database_dump_command_ok_handler(self): print('Database contents dumped')
class ContaAddCommand(object): def __init__(self): self._dao = Inject('conta dao') @staticmethod def conta_parser_created_handler(conta_parser): conta_add_parser = conta_parser.add_parser('add', aliases=['a'], help='Adiciona uma nova conta') conta_add_parser.set_defaults(event='conta_add_command') conta_add_parser.add_argument("-c", "--contabilizavel", default=False, action="store_true", help="Marca a nova conta como contabilizável (o saldo é calculado nas listagens)") conta_add_parser.add_argument("--df", "--fechamento", help="Data de fechamento da conta") conta_add_parser.add_argument("nome", help="Nome da conta a ser criada") def conta_add_command_handler(self, args): conta = Conta() conta.nome = args.nome if args.contabilizavel: conta.propriedades += 'contabilizável' if hasattr(args, "fechamento"): conta.fechamento = args.fechamento self._dao.insert(conta) return 'ok', conta def conta_add_command_ok_handler(self, conta): message = 'Conta "{}"'.format(conta.nome) if conta.propriedades != '': message += ' com as propriedades "{}"'.format(conta.propriedades) message += " criada." print(message)
class ConnectionFactory(object): def __init__(self): self._configs = Inject('app configuration') self._logger = Inject('logger') @staticmethod def get_external_resources(): return [{ 'name': 'database connection', 'creator': ConnectionFactory.create_connection }] def create_connection(self): # TODO Make this more generic, should not use fdc keys here! self._logger.debug('Connecting to database at {}...', self._configs['fdc.db_full_path']) connection = sqlite3.connect(self._configs['fdc.db_full_path']) sqlite3.register_adapter(decimal.Decimal, lambda d: str(d)) sqlite3.register_converter('decimal', lambda s: decimal.Decimal(s)) return connection
class LancamentoBalanceHandler(object): def __init__(self): self._lancamento_dao = Inject('lancamento dao') def lancamento_balance_command_handler(self, args): # TODO Limit this by date lancamentos = self._lancamento_dao.list_with_contas() lancamentos_per_day = self._group_lancamentos_per_day(lancamentos) saldos = dict() balance = dict() for data, lancamentos in lancamentos_per_day.items(): for lancamento in lancamentos: self._update_balance(balance, lancamento.origem, -lancamento.valor) self._update_balance(balance, lancamento.destino, lancamento.valor) saldos[data] = dict(balance) contas = list(set(balance.keys())) contas.sort() return 'ok', {'saldos': saldos, 'contas': contas} def _group_lancamentos_per_day(self, lancamentos): lancamentos_per_day = {} for lancamento in lancamentos: day_lancamentos = lancamentos_per_day.setdefault( lancamento.data, []) day_lancamentos.append(lancamento) return lancamentos_per_day def _update_balance(self, diario, conta, valor): if 'contabilizável' in conta.propriedades: saldo = diario.setdefault(conta.nome, Decimal(0)) saldo += Decimal(valor) / Decimal(100) diario[conta.nome] = saldo
class ImportCSVCommand(object): def __init__(self): self._connection = Inject('database connection') self._conta_dao = Inject('conta dao') self._produto_dao = Inject('produto dao') self._fornecedor_dao = Inject('fornecedor dao') self._lancamento_dao = Inject('lancamento dao') self._unknown_contas = None self._unknown_produtos = None self._unknown_fornecedores = None @staticmethod def import_parser_created_handler(import_parser): import_csv_parser = import_parser.add_parser('csv', help='Importa um arquivo .csv') import_csv_parser.set_defaults(event='import_csv_command') import_csv_parser.add_argument('-c', '--confirm', action='store_true', help='Confirma a importação, caso contrário apenas mostra em tela o que será importado') import_csv_parser.add_argument("filename", help="Nome do arquivo que será importado") def import_csv_command_handler(self, args): source = open(args.filename, 'r') self._unknown_contas = set() self._unknown_produtos = set() self._unknown_fornecedores = set() ok = True lancamentos = [] try: for self._i, self._line in enumerate(source): fields_array = self._get_fields_array() if not self._validate(fields_array, lambda f: len(f) >= 4, None, 'every line must have at least 4 fields'): continue fields = self._get_fields(fields_array) ok &= self._validate_fields(*fields) if ok: lancamento = self._make_lancamento(*fields) if args.confirm: self._lancamento_dao.insert(lancamento) else: lancamentos.append(lancamento) except Exception: self._connection.rollback() raise if ok: if args.confirm: self._connection.commit() return 'imported', {'filename': args.filename} else: return 'simulated', {'filename': args.filename, 'lancamentos': lancamentos} else: if args.confirm: self._connection.rollback() return 'error', {'filename': args.filename, 'unknown_contas': self._unknown_contas, 'unknown_produtos': self._unknown_produtos, 'unknown_fornecedores': self._unknown_fornecedores} def _get_fields_array(self): if self._line.endswith('\n'): self._line = self._line[:-1] fields = self._line.split(';') return fields def _validate(self, data, validator, validation_fail_callback, message): if not validator(data): print('[ERROR] Line {}: {}'.format(self._i + 1, message.format(data))) if validation_fail_callback: validation_fail_callback() return False return True def _get_fields(self, fields_array): data = fields_array[0] origem = fields_array[1] destino = fields_array[2] valor = fields_array[3] observacoes = self._get(fields_array, 4) produto = self._get(fields_array, 5) quantidade = self._get(fields_array, 6) fornecedor = self._get(fields_array, 7) return data, origem, destino, valor, observacoes, produto, quantidade, fornecedor def _validate_fields(self, data, origem, destino, valor, observacoes, produto, quantidade, fornecedor): ok = self._validate(data, self._data_ok, None, 'date "{{}}" is not in format "{}"'.format(_CSV_DATE_FORMAT)) ok &= self._validate(origem, self._conta_dao.by_name, lambda: self._unknown_contas.add(origem), 'origem "{}" doesnt exists') ok &= self._validate(destino, self._conta_dao.by_name, lambda: self._unknown_contas.add(destino), 'destino "{}" doesnt exists') ok &= self._validate(valor, self._valor_ok, None, 'valor "{}" is not in valid format') if produto: ok &= self._validate(produto, self._produto_dao.by_name, lambda: self._unknown_produtos.add(produto), 'produto "{}" not found') if quantidade: ok &= self._validate(quantidade, self._is_float, None, 'quantidade "{}" is not a float value') if fornecedor: ok &= self._validate(fornecedor, self._fornecedor_dao.by_name, lambda: self._unknown_fornecedores.add(fornecedor), 'fornecedor "{}" not found') return ok def _make_lancamento(self, data, origem, destino, valor, observacoes, produto, quantidade, fornecedor): lancamento = Lancamento() # TODO Extract a formatter from the following two lines lancamento.data = datetime.strftime(datetime.strptime(data, _CSV_DATE_FORMAT), _SQLITE_DATE_FORMAT) lancamento.valor = int(valor.replace('.', '')) lancamento.origem = self._conta_dao.by_name(origem) lancamento.destino = self._conta_dao.by_name(destino) lancamento.observacoes = observacoes lancamento.produto = self._produto_dao.by_name(produto) if produto else None lancamento.quantidade = quantidade lancamento.fornecedor = self._fornecedor_dao.by_name(fornecedor) if fornecedor else None return lancamento @staticmethod def _get(fields, index, default=None): return fields[index] if len(fields) >= index + 1 else default @staticmethod def _data_ok(value): try: datetime.strptime(value, _CSV_DATE_FORMAT) return True except ValueError: return False @staticmethod def _valor_ok(valor): try: # TODO Internacionalize this to work with , for decimal point! Decimal(valor) return True except InvalidOperation: return False @staticmethod def _is_float(value): try: float(value) return True except ValueError: return False @staticmethod def import_csv_command_imported_handler(filename): print('Arquivo "{}" importado com sucesso!'.format(filename)) @staticmethod def import_csv_command_simulated_handler(filename, lancamentos): print('Simulated import for file "{}" (use --confirm to actually import), would create lancamentos:'.format( filename)) for lancamento in lancamentos: print(ImportCSVCommand._make_lancamento_string(lancamento)) @staticmethod def _make_lancamento_string(lancamento): values = list() values.append(str(lancamento.data)) values.append(str(lancamento.origem)) values.append(str(lancamento.destino)) values.append(str(lancamento.valor)) values.append(str(lancamento.observacoes)) if lancamento.observacoes else None values.append(str(lancamento.produto)) if lancamento.produto else None values.append(str(lancamento.quantidade)) if lancamento.quantidade else None values.append(str(lancamento.fornecedor)) if lancamento.fornecedor else None return '; '.join(values) @staticmethod def import_csv_command_error_handler(filename, unknown_contas, unknown_produtos, unknown_fornecedores): print('Erros importando arquivo "{}"!'.format(filename)) print() ImportCSVCommand._print_array('Contas desconhecidas:', unknown_contas) ImportCSVCommand._print_array('Produtos desconhecidos:', unknown_produtos) ImportCSVCommand._print_array('Fornecedores desconhecidos:', unknown_fornecedores) @staticmethod def _print_array(message, array): if len(array) == 0: return print(message) for conta in array: print(conta) print()
class DatabaseRestoreCommand(object): def __init__(self): self._config = Inject('app configuration') self._logger = Inject('logger') self._git = Inject('fdc git wrapper') def database_parser_created_handler(self, db_parser): db_parser.add_parser("restore", help="Restore fdc.dump to fdc.db").set_defaults( event='database_restore_command') # TODO A flag to skip git pull # TODO A flag to rename existing database # TODO A parameter to change dump file location # TODO A parameter to change database to be restored # TODO A parameter to disable git pull def database_restore_command_handler(self, args): self._git.pull() # FIXME This will become huge after some time..... restore_database_script = self._get_database_restore_script() if not restore_database_script: return 'dump_file_not_found' connection = self._prepare_connection() # FIXME Rollback on error connection.executescript(restore_database_script) connection.commit() connection.close() return 'ok' def _get_database_restore_script(self): if not os.path.isfile(self._config['fdc.dump_full_path']): return None file = open(self._config['fdc.dump_full_path'], 'r') script = file.read() file.close return script def _prepare_connection(self): if os.path.isfile(self._config['fdc.db_full_path']): os.remove(self._config['fdc.db_full_path']) return di_container.get_resource('database connection') def database_restore_command_dump_file_not_found_handler(self): print('Dump file not found at "{}". Nothing to do.'.format( self._config['fdc.db_full_path'])) def database_restore_command_ok_handler(self): print('Dump file at "{}" restored into database "{}" successfully.'. format(self._config['fdc.dump_full_path'], self._config['fdc.db_full_path']))
def __init__(self): self._config = Inject('app configuration') self._logger = Inject('logger') self._git = Inject('fdc git wrapper')
def __init__(self): self._dependency = Inject('transient_dependency')
def __init__(self): self._configs = Inject('app configuration') self._logger = Inject('logger')
def __init__(self): self._was_set = Inject('dependency_of_external_dependency_was_set')
def __init__(self): self._conta_dao = Inject('conta dao') self._lancamento_dao = Inject('lancamento dao')
def __init__(self): self._lancamento_dao = Inject('lancamento dao')
def __init__(self): self._dao = Inject('conta dao')
def __init__(self, entity_class, metadata): self._connection = Inject('database connection') self._logger = Inject('logger') self._entity_class = entity_class self._metadata = metadata
class FDCInitCommand(object): def __init__(self): self._configs = Inject('app configuration') self._git = Inject('fdc git wrapper') self._logger = Inject('logger') def database_parser_created_handler(self, db_parser): db_parser.add_parser( "init", help= "Inicializa a pasta do FDC e o banco de dados com uma estrutura vazia. Se houver um banco de dados já " "existente o mesmo será excluído e reiniciado.").set_defaults( event='database_init_command') def database_init_command_handler(self, args): # TODO Verificar se o arquivo de banco de dados é um SQLITE válido! self._ensure_fdc_folder_exists() self._ensure_fdc_folder_is_git_repository() self._ensure_database_structure_with_no_data() return 'ok' def _ensure_fdc_folder_exists(self): if not os.path.isdir(self._configs['fdc.folder']): self._logger.info('Creating fdc folder at "{}"', self._configs['fdc.folder']) os.makedirs(self._configs['fdc.folder'], exist_ok=True) else: self._logger.info('fdc folder found at "{}"', self._configs['fdc.folder']) def _ensure_fdc_folder_is_git_repository(self): if not self._git.is_repository(): self._logger.info( 'Git repository not found at fdc folder, creating it...') if not self._git.init(): raise FDCInitializationException( 'Error initializing git repository at "{}"', self._git.repository_folder) def _ensure_database_structure_with_no_data(self): if os.path.isfile(self._configs['fdc.db_full_path']): self._logger.warn('Database file "{}" found, removing it...', self._configs['fdc.db_full_path']) os.remove(self._configs['fdc.db_full_path']) connection = di_container.get_resource('database connection') # TODO Create these tables via controller events to centralize table handling in specialized classes self._logger.debug('Creating table "Conta"...') connection.executescript('create table Conta (' ' nome text not null,' ' descricao text,' ' data_aquisicao date,' ' propriedades text not null,' ' observacao text);') self._logger.debug('Creating table "Cotacao"...') connection.executescript('create table Cotacao (' ' data date not null,' ' moeda text not null,' ' valor integer not null);') self._logger.debug('Creating table "Orcamento"...') connection.executescript('create table Orcamento (' ' nome text not null,' ' descricao text not null);') self._logger.debug('Creating table "OrcamentoLancamento"...') connection.executescript('create table OrcamentoLancamento (' ' orcamento not null references Orcamento,' ' regra_data_vencimento text not null,' ' origem_padrao not null references Conta,' ' destino_padrao not null references Conta,' ' valor_padrao integer not null,' ' cotacao_moeda text,' ' regra_cotacao_data text,' ' regra_periodo_referencia text not null,' ' produto references Produto,' ' quantidade integer,' ' observacao text);') self._logger.debug('Creating table "Lancamento"...') connection.executescript('create table Lancamento (' ' data date not null,' ' origem references Conta not null,' ' destino references Conta not null,' ' valor integer not null,' ' cotacao references Cotacao,' ' referencia_inicio text,' ' referencia_fim text,' ' realizado boolean not null default false,' ' produto references Produto,' ' fornecedor references Fornecedor,' ' quantidade integer,' ' observacao text);') self._logger.debug('Creating table "Produto"...') connection.executescript('create table Produto (' ' nome text not null,' ' medida text,' ' unidade text);') self._logger.debug('Creating table "Fornecedor"...') connection.executescript('create table Fornecedor (' ' nome text not null);') # TODO Handle errors def database_init_command_ok_handler(self): print('Structure created successfully')
def __init__(self): self._external_dependency = Inject('external_dependency')
def __init__(self): self._dependency = Inject('dependency_name')
def __init__(self): self._dependency = Inject('dependency which doesnt exists')
class Main(object): def __init__(self): self._logger = Inject('logger') self._configs = Inject('app configuration') def main(self): packages = [ 'commons', 'di_container', __package__ + '.commons', __package__ + '.conta', __package__ + '.database', __package__ + '.import', 'fdc.lancamento', 'fdc.produto', 'fdc.fornecedor', ] di_container.load_resources(packages) controller.load_listeners(packages) di_container.inject_resources(self) self._logger.info('Loaded resources and listeners from packages: {}', packages) self._logger.debug('Using configurations: {}', self._configs.dump()) args = self.parse_command_line() self._logger.info('Command line parsed: {}', args) event_results = controller.event(args.event, inject_dependencies=True, args=args) for result in event_results: self._logger.info( 'Calling front end for "{}" with data "{}, {}" due to status "{}"', args.event, result.kwdata, result.data, result.status) # inject_dependencies parameter put first event_args = True, *result.data controller.event(args.event + '_' + result.status, *event_args, **result.kwdata) def parse_command_line(self): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() controller.event('root_parser_created', root_parser=subparsers) args = parser.parse_args() if not hasattr(args, "event"): parser.print_help() parser.exit(0, "No arguments supplied\n") else: return args
class AbstractDao(object): def __init__(self, entity_class, metadata): self._connection = Inject('database connection') self._logger = Inject('logger') self._entity_class = entity_class self._metadata = metadata def new_builder(self, kind): if kind == 'SELECT': return SelectBuilder(self._metadata) else: return None def get_single(self, where=None, *values): list = self.list(where, *values) return list[0] if len(list) > 0 else None def list(self, where=None, *values): cursor = self._create_select_cursor((), where, *values) entity_list = self._load_cursor(cursor) cursor.close() return entity_list def exists(self, where, *values): cursor = self._create_select_cursor(('count(*)', ), where, *values) count = cursor.fetchone()[0] return count > 0 def _create_select_cursor(self, fields=(), where=None, *values): builder = self.new_builder('SELECT') builder.where = where builder.fields(*fields) if len(fields) > 0 else None query = builder.build() return self._connection.execute(query, values) def _load_cursor(self, cursor): entity_list = [] for row in cursor: entity = self._load_row(row) entity_list.append(entity) return entity_list def _load_row(self, row): entity_values = {} for i, field_name in enumerate(self._metadata.fields): value = row[i] entity_values[field_name] = value return SimpleNamespace(**entity_values) def insert(self, entity): builder = InsertBuilder(self._metadata) sql = builder.build() # TODO Essa recuperação de valores é responsabilidade do InsertBuilder values = self._get_field_values(entity) self._logger.debug('Running "{}" with values "{}"', sql, values) self._connection.execute(sql, values) self._connection.commit() def _get_field_values(self, entity): t = () for field in self._metadata.fields: if field == 'rowid': continue value = getattr(entity, field, None) if hasattr(value, 'rowid'): t += value.rowid, else: t += value, return t
def __init__(self): self._configs = Inject('app configuration') self._verbosity_level = None
def __init__(self): self._dependency_with_transient = Inject('dependency_with_transient')