📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Módulo 02: Lógica de Programação e Algoritmos. Recomendado revisar antes de começar.
Trilha de Especialização — Nível Introdutório (Curso 25: Sistemas Desktop com Python)
- Visão Geral dos 10 Miniprojetos: 🌱 Guia Pedagógico de Miniprojetos
- Catálogo Geral: 🏗️ Catálogo de Projetos
- ➡️ Implementação Prática: Miniprojeto 02 (Código Completo em 4 Degraus)
Nas mercearias, padarias e quitandas de cidades pequenas e bairros, o controle de compras a prazo (“caderneta de fiado”) ainda é frequentemente feito em cadernos de papel sujeitos a perdas, rasuras ou erros de soma.
Neste projeto, o estudante aprende como:
.json aninhados com json.dump() e json.load().Cliente com métodos para adicionar compras e abater pagamentos, organizando a lógica de negócio.clientes e movimentacoes_fiado) e calcular saldos dinâmicos com SUM().flowchart LR
D1["🪜 DEGRAU 1\nImperativo & JSON\nMenu while + fiado.json\ncom json.dump() e load()"] --> D2["🪜 DEGRAU 2\nPrimeira Classe POO\nclass Cliente\n(adicionar_compra, pagar)"]
D2 --> D3["🪜 DEGRAU 3\nSQLite Relacional (1:N)\nTabelas clientes e\nmovimentacoes com FK"]
D3 --> D4["🪜 DEGRAU 4\nInterface Web Streamlit\nPainel financeiro, métricas,\nformulários e extratos"]
style D1 fill:#fffde7,stroke:#fbc02d,stroke-width:2px
style D2 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style D3 fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
style D4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
sistemas_python_intro_02_fiado_padaria/
├── degrau_1_imperativo_json/
│ ├── main.py # Menu imperativo com dicionários e fiado.json
│ └── test_degrau_1.py # Testes automatizados unitários
├── degrau_2_primeira_classe/
│ ├── main.py # Primeira classe POO (class Cliente)
│ └── test_degrau_2.py # Testes unitários da classe Cliente
├── degrau_3_sqlite/
│ ├── main.py # Banco SQLite3 com 2 tabelas e chave estrangeira (1:N)
│ └── test_degrau_3.py # Testes unitários de integridade relacional
├── degrau_4_interface_streamlit/
│ ├── app.py # Interface Web moderna em Python puro com Streamlit
│ └── main.py # Launcher para iniciar o servidor web local
├── main.py # Hub Central / Launcher interativo
└── index.md # Este guia didático autoguiado
Diferente do Miniprojeto 01 (que não usava nenhuma biblioteca externa), este projeto tem uma dependência real de terceiros no Degrau 4: o pacote streamlit. Os Degraus 1, 2 e 3 continuam 100% Standard Library — só o Degrau 4 exige um pip install.
| Requisito | Versão / Observação |
|---|---|
| Python | 3.8 ou superior (testado com 3.11) |
| Bibliotecas nativas usadas (Degraus 1-3) | json, os, sqlite3, subprocess, sys, unittest — já inclusas em qualquer instalação padrão do Python, nenhum pip install necessário |
| Dependência externa (Degrau 4) | streamlit — versão mais recente estável no momento em que este guia foi escrito: 1.62.0 (confira a sua com pip show streamlit) |
| Como instalar | pip install streamlit (ou pip install streamlit==1.62.0 para fixar exatamente essa versão) |
requirements.txt |
Não existe neste miniprojeto ainda; se quiser fixar a versão, crie um arquivo requirements.txt na raiz do projeto contendo a linha streamlit==1.62.0 e instale com pip install -r requirements.txt |
| Editor recomendado | VS Code ou PyCharm, mas qualquer terminal com python no PATH funciona |
[!TIP] Se
python --versionnão funcionar no seu terminal, tentepython3 --version. Em alguns sistemas o executável do Python 3 é registrado apenas comopython3(o mesmo vale parapip→pip3).Para conferir se o Streamlit já está instalado e qual versão, sem precisar abrir o app:
pip show streamlitSe aparecer
WARNING: Package(s) not found, é porque ainda falta instalar — rodepip install streamlit.
[!NOTE] Você não precisa instalar nada para rodar os Degraus 1, 2 e 3 (terminal + JSON + SQLite). O
pip install streamlitsó é necessário quando for rodar o Degrau 4 (streamlit run degrau_4_interface_streamlit/app.py). O launcherdegrau_4_interface_streamlit/main.pyinclusive detecta automaticamente se o Streamlit não está instalado e te mostra o comando exato a rodar (veja a seção do Degrau 4 mais abaixo).
No VS Code / PyCharm, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd sistemas_python_intro_02_fiado_padaria
# Executar o Hub Central (Menu interativo com todos os degraus):
python main.py
# Ou executar os degraus de terminal (1 a 3):
python degrau_1_imperativo_json/main.py
python degrau_2_primeira_classe/main.py
python degrau_3_sqlite/main.py
# Executar a aplicação Web (Degrau 4 com Streamlit):
streamlit run degrau_4_interface_streamlit/app.py
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta executar:
python catalogo_projetos/sistemas_python_intro_02_fiado_padaria/main.py
.json)No primeiro degrau, armazenamos os clientes em um dicionário Python onde a chave é o nome do cliente e o valor contém seus dados e histórico de compras. O arquivo fiado.json preserva as informações entre execuções.
import json — módulo nativo do Python para converter entre estruturas Python (dicionários, listas) e o formato de texto JSON. É o “irmão” do open() usado no Miniprojeto 01, mas voltado a dados estruturados em vez de linhas simples separadas por vírgula.json.load(f) — lê o conteúdo de um arquivo JSON já aberto e devolve a estrutura Python equivalente (aqui, um dicionário).json.dump(dados, f, indent=4, ensure_ascii=False) — grava um dicionário/lista Python no arquivo em formato JSON. indent=4 deixa o arquivo formatado (fácil de ler abrindo no editor); ensure_ascii=False mantém acentos como ç/ã legíveis em vez de convertê-los para códigos como ç.try / except json.JSONDecodeError — sua primeira captura de exceção neste guia. Protege contra o caso do arquivo fiado.json existir mas estar corrompido/vazio (o que faria json.load() lançar um erro); nesse caso, o programa simplesmente recomeça com um dicionário vazio em vez de travar.dados[nome] = {"telefone": ..., "saldo_devedor": ..., "historico": [...]}) — diferente do dicionário “raso” do Miniprojeto 01 ({"descricao": ..., "status": ...}), aqui cada cliente guarda, dentro de si, uma lista de outros dicionários (o histórico de compras/pagamentos). É um dicionário dentro de um dicionário, com uma lista no meio.nome not in dados — testa se uma chave existe num dicionário (equivalente a perguntar “esse cliente já está cadastrado?”), já que aqui o nome do cliente é a própria chave do dicionário principal.float(val_txt.replace(",", ".")) — converte texto digitado pelo usuário em número decimal, aceitando tanto 15.50 quanto 15,50 (comum no Brasil) antes de tentar o float().degrau_1_imperativo_json/main.py):import json
import os
ARQUIVO_PADRAO = "fiado.json"
def carregar_dados(caminho=ARQUIVO_PADRAO):
if not os.path.exists(caminho):
return {}
try:
with open(caminho, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
return {}
def salvar_dados(dados, caminho=ARQUIVO_PADRAO):
with open(caminho, "w", encoding="utf-8") as f:
json.dump(dados, f, indent=4, ensure_ascii=False)
def cadastrar_cliente(dados, nome, telefone):
nome, telefone = nome.strip(), telefone.strip()
if not nome:
print("[ERRO] O nome do cliente nao pode ser vazio.")
return False
if nome in dados:
print(f"[AVISO] O cliente '{nome}' ja esta cadastrado.")
return False
dados[nome] = {"telefone": telefone, "saldo_devedor": 0.0, "historico": []}
print(f"[OK] Cliente '{nome}' cadastrado com sucesso!")
return True
def adicionar_compra(dados, nome, valor, descricao="Compra no balcao"):
if nome not in dados:
print(f"[ERRO] Cliente '{nome}' nao encontrado.")
return False
if valor <= 0:
print("[ERRO] O valor da compra deve ser maior que zero.")
return False
dados[nome]["saldo_devedor"] += valor
dados[nome]["historico"].append({"tipo": "COMPRA", "valor": valor, "descricao": descricao})
print(f"[OK] Compra de R$ {valor:.2f} anotada para '{nome}'. Saldo: R$ {dados[nome]['saldo_devedor']:.2f}")
return True
def registrar_pagamento(dados, nome, valor):
if nome not in dados:
print(f"[ERRO] Cliente '{nome}' nao encontrado.")
return False
if valor <= 0:
print("[ERRO] O valor do pagamento deve ser maior que zero.")
return False
dados[nome]["saldo_devedor"] -= valor
dados[nome]["historico"].append({"tipo": "PAGAMENTO", "valor": valor, "descricao": "Abatimento de conta"})
print(f"[OK] Pagamento de R$ {valor:.2f} registrado! Saldo restante: R$ {dados[nome]['saldo_devedor']:.2f}")
return True
def listar_devedores(dados):
if not dados:
print("\n[INFO] Nenhum cliente cadastrado na caderneta.")
return
print("\n--- CADERNETA DE FIADO (CLIENTES) ---")
total_a_receber = 0.0
for nome, info in dados.items():
saldo = info["saldo_devedor"]
total_a_receber += saldo
status = "[DEVENDO]" if saldo > 0 else "[QUITADO]"
print(f"{status} {nome} (Tel: {info['telefone']}) -> Saldo Devedor: R${saldo:.2f}") print("-------------------------------------") print(f"TOTAL GERAL A RECEBER NO MES: R$ {total_a_receber:.2f}\n")
def menu_principal():
"""Loop principal do menu interativo no terminal."""
dados = carregar_dados()
while True:
print("\n=== CADERNO DE FIADO (DEGRAU 1: IMPERATIVO & JSON) ===")
print("1. Cadastrar Novo Cliente")
print("2. Anotar Nova Compra (Fiado)")
print("3. Registrar Pagamento (Abater Dívida)")
print("4. Listar Todos os Clientes e Saldos")
print("5. Sair")
opcao = input("Escolha uma opcao (1-5): ").strip()
if opcao == "1":
nome = input("Nome do cliente: ")
tel = input("Telefone / WhatsApp: ")
if cadastrar_cliente(dados, nome, tel):
salvar_dados(dados)
elif opcao == "2":
nome = input("Nome do cliente: ")
val_txt = input("Valor da compra (R$): ") desc = input("Descricao dos itens (ex: 2 paes, 1 leite): ") try: val = float(val_txt.replace(",", ".")) if adicionar_compra(dados, nome, val, desc): salvar_dados(dados) except ValueError: print("[ERRO] Digite um valor numerico valido (ex: 15.50).") elif opcao == "3": nome = input("Nome do cliente: ") val_txt = input("Valor do pagamento (R$): ")
try:
val = float(val_txt.replace(",", "."))
if registrar_pagamento(dados, nome, val):
salvar_dados(dados)
except ValueError:
print("[ERRO] Digite um valor numerico valido.")
elif opcao == "4":
listar_devedores(dados)
elif opcao == "5":
salvar_dados(dados)
print("[INFO] Dados gravados com sucesso no arquivo fiado.json. Ate logo!")
break
else:
print("[ERRO] Opcao invalida. Digite de 1 a 5.")
if __name__ == "__main__":
menu_principal()
carregar_dados() / salvar_dados(dados) — o par que troca entre “arquivo em disco” e “dicionário em memória”. Note a diferença para o Miniprojeto 01: lá, cada linha do .txt era montada manualmente com f"{desc},{status}\n"; aqui, json.dump/json.load fazem toda a conversão sozinhos, mesmo com estruturas aninhadas (listas dentro de dicionários).cadastrar_cliente — usa o nome como chave única do dicionário principal. Por isso o teste if nome in dados: é suficiente para saber se aquele cliente já existe — não precisa percorrer nada manualmente.adicionar_compra / registrar_pagamento — seguem o mesmo padrão: validam a existência do cliente e o valor, depois alteram o saldo e acrescentam uma entrada no histórico (dados[nome]["historico"].append({...})). É esse histórico que, em degraus futuros de outros projetos, permitiria reconstruir um extrato completo.listar_devedores — percorre o dicionário com .items() (que devolve pares chave, valor — aqui, nome, info) e acumula total_a_receber conforme itera, exibindo o resumo geral no final.menu_principal() — segue exatamente a mesma estrutura de loop while True do Miniprojeto 01, mas agora tratando também erros de digitação de valores monetários com try/except ValueError em cada operação financeira.=== CADERNO DE FIADO (DEGRAU 1: IMPERATIVO & JSON) ===
1. Cadastrar Novo Cliente
2. Anotar Nova Compra (Fiado)
3. Registrar Pagamento (Abater Dívida)
4. Listar Todos os Clientes e Saldos
5. Sair
Escolha uma opcao (1-5): 1
Nome do cliente: Seu Zé da Farmácia
Telefone / WhatsApp: 99999-1111
[OK] Cliente 'Seu Zé da Farmácia' cadastrado com sucesso!
Escolha uma opcao (1-5): 2
Nome do cliente: Seu Zé da Farmácia
Valor da compra (R$): 25,50 Descricao dos itens (ex: 2 paes, 1 leite): Pão de queijo e café [OK] Compra de R$ 25.50 anotada para 'Seu Zé da Farmácia'. Saldo: R$ 25.50
Escolha uma opcao (1-5): 4
--- CADERNETA DE FIADO (CLIENTES) ---
[DEVENDO] Seu Zé da Farmácia (Tel: 99999-1111) -> Saldo Devedor: R$25.50 ------------------------------------- TOTAL GERAL A RECEBER NO MES: R$ 25.50
[!WARNING] ⚠️ Erros comuns de quem está começando neste degrau:
- Digitar
25,50(com vírgula) e esquecer que o Python usa ponto decimal — o código já trata isso com.replace(",", "."), mas se você reaproveitar esse padrão em outro exercício sem essa linha,float("25,50")geraValueError.- Confundir “chave não existe” com “dicionário vazio” —
if nome not in dados:verifica uma chave específica;if not dados:verifica se o dicionário inteiro está vazio. São testes diferentes, usados em pontos diferentes do código.- Esquecer que
.append()em uma lista dentro de um dicionário altera o dicionário original — comodados[nome]["historico"]é uma lista (um objeto “mutável”),dados[nome]["historico"].append({...})já modificadadosdiretamente; não é necessário reatribuir nada.- Editar
fiado.jsonmanualmente e cometer um erro de sintaxe (por exemplo, esquecer uma vírgula) — na próxima execução,json.load()lançariajson.JSONDecodeError; é exatamente para esse cenário que existe otry/exceptemcarregar_dados().
[!TIP] 🧩 Desafio extra (opcional): adicione uma quinta opção “Ver histórico de um cliente”, que imprima cada item de
dados[nome]["historico"](tipo, valor e descrição) usando umforsimples.
degrau_1_imperativo_json/test_degrau_1.py):"""
Testes Unitários para o Degrau 1 do Projeto 02 (Imperativo + JSON)
"""
import os
import unittest
from main import (
adicionar_compra,
cadastrar_cliente,
carregar_dados,
registrar_pagamento,
salvar_dados
)
ARQUIVO_TESTE = "teste_fiado.json"
class TestDegrau1Fiado(unittest.TestCase):
def setUp(self):
if os.path.exists(ARQUIVO_TESTE):
os.remove(ARQUIVO_TESTE)
def tearDown(self):
if os.path.exists(ARQUIVO_TESTE):
os.remove(ARQUIVO_TESTE)
def test_cadastrar_cliente_e_compras(self):
dados = {}
sucesso = cadastrar_cliente(dados, "Seu Ze da Farmacia", "99999-1111")
self.assertTrue(sucesso)
self.assertIn("Seu Ze da Farmacia", dados)
# Compra fiado
adicionar_compra(dados, "Seu Ze da Farmacia", 25.50, "Pao de queijo e cafe")
self.assertEqual(dados["Seu Ze da Farmacia"]["saldo_devedor"], 25.50)
# Salvamento JSON
salvar_dados(dados, caminho=ARQUIVO_TESTE)
carregados = carregar_dados(caminho=ARQUIVO_TESTE)
self.assertEqual(carregados["Seu Ze da Farmacia"]["saldo_devedor"], 25.50)
def test_pagamento_e_abatimento(self):
dados = {}
cadastrar_cliente(dados, "Dona Maria", "98888-2222")
adicionar_compra(dados, "Dona Maria", 50.00)
registrar_pagamento(dados, "Dona Maria", 20.00)
self.assertEqual(dados["Dona Maria"]["saldo_devedor"], 30.00)
self.assertEqual(len(dados["Dona Maria"]["historico"]), 2)
if __name__ == "__main__":
unittest.main()
Cliente)No segundo degrau, transformamos o cliente em um objeto da classe Cliente, encapsulando os métodos adicionar_compra() e pagar_conta().
degrau_2_primeira_classe/main.py):import json
import os
ARQUIVO_PADRAO = "fiado_poo.json"
class Cliente:
"""Representa um cliente com sua respectiva conta de fiado."""
def __init__(self, nome, telefone, saldo_devedor=0.0):
self.nome = nome
self.telefone = telefone
self.saldo_devedor = float(saldo_devedor)
def adicionar_compra(self, valor):
if valor > 0:
self.saldo_devedor += valor
return True
return False
def pagar_conta(self, valor):
if valor > 0:
self.saldo_devedor -= valor
return True
return False
def obter_status(self):
return "Devendo" if self.saldo_devedor > 0 else "Quitado"
def para_dicionario(self):
return {
"nome": self.nome,
"telefone": self.telefone,
"saldo_devedor": self.saldo_devedor
}
@staticmethod
def de_dicionario(d):
return Cliente(
nome=d["nome"],
telefone=d["telefone"],
saldo_devedor=d.get("saldo_devedor", 0.0)
)
degrau_2_primeira_classe/test_degrau_2.py):"""
Testes Unitários para o Degrau 2 do Projeto 02 (Primeira Classe POO)
"""
import os
import unittest
from main import Cliente, carregar_clientes, salvar_clientes
ARQUIVO_TESTE = "teste_fiado_poo.json"
class TestDegrau2POO(unittest.TestCase):
def setUp(self):
if os.path.exists(ARQUIVO_TESTE):
os.remove(ARQUIVO_TESTE)
def tearDown(self):
if os.path.exists(ARQUIVO_TESTE):
os.remove(ARQUIVO_TESTE)
def test_classe_cliente_metodos(self):
c = Cliente("Marcio Pedreiro", "97777-3333", saldo_devedor=0.0)
self.assertEqual(c.saldo_devedor, 0.0)
self.assertEqual(c.obter_status(), "Quitado")
c.adicionar_compra(75.50)
self.assertEqual(c.saldo_devedor, 75.50)
self.assertEqual(c.obter_status(), "Devendo")
c.pagar_conta(25.50)
self.assertEqual(c.saldo_devedor, 50.00)
def test_salvar_e_carregar_objetos(self):
c1 = Cliente("Ana Professora", "98111-4444", 30.00)
c2 = Cliente("Carlos Mecanico", "98222-5555", 0.00)
salvar_clientes([c1, c2], caminho=ARQUIVO_TESTE)
carregados = carregar_clientes(caminho=ARQUIVO_TESTE)
self.assertEqual(len(carregados), 2)
self.assertEqual(carregados[0].nome, "Ana Professora")
self.assertEqual(carregados[0].saldo_devedor, 30.00)
self.assertEqual(carregados[1].nome, "Carlos Mecanico")
if __name__ == "__main__":
unittest.main()
No terceiro degrau, criamos uma estrutura relacional profissional no SQLite: a tabela clientes armazena os dados cadastrais e a tabela movimentacoes_fiado armazena cada compra e pagamento com FOREIGN KEY(cliente_id) REFERENCES clientes(id).
degrau_3_sqlite/main.py):import sqlite3
BANCO_PADRAO = "fiado.db"
def conectar_banco(caminho=BANCO_PADRAO):
conn = sqlite3.connect(caminho)
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def inicializar_tabelas(caminho=BANCO_PADRAO):
conn = conectar_banco(caminho)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS clientes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL UNIQUE,
telefone TEXT
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS movimentacoes_fiado (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cliente_id INTEGER NOT NULL,
tipo TEXT NOT NULL CHECK(tipo IN ('COMPRA', 'PAGAMENTO')),
valor REAL NOT NULL,
descricao TEXT,
data_registro TEXT DEFAULT (datetime('now', 'localtime')),
FOREIGN KEY (cliente_id) REFERENCES clientes(id) ON DELETE CASCADE
)
""")
conn.commit()
conn.close()
def cadastrar_cliente(nome, telefone, caminho=BANCO_PADRAO):
try:
conn = conectar_banco(caminho)
cursor = conn.cursor()
cursor.execute("INSERT INTO clientes (nome, telefone) VALUES (?, ?)", (nome.strip(), telefone.strip()))
cliente_id = cursor.lastrowid
conn.commit()
conn.close()
return cliente_id
except sqlite3.IntegrityError:
return None
def registrar_movimentacao(cliente_id, tipo, valor, descricao="", caminho=BANCO_PADRAO):
if valor <= 0:
return False
conn = conectar_banco(caminho)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO movimentacoes_fiado (cliente_id, tipo, valor, descricao)
VALUES (?, ?, ?, ?)
""", (cliente_id, tipo, valor, descricao))
conn.commit()
conn.close()
return True
def listar_clientes_com_saldo(caminho=BANCO_PADRAO):
conn = conectar_banco(caminho)
cursor = conn.cursor()
cursor.execute("""
SELECT
c.id,
c.nome,
c.telefone,
COALESCE(SUM(CASE WHEN m.tipo = 'COMPRA' THEN m.valor ELSE -m.valor END), 0.0) AS saldo
FROM clientes c
LEFT JOIN movimentacoes_fiado m ON c.id = m.cliente_id
GROUP BY c.id, c.nome, c.telefone
ORDER BY c.nome ASC
""")
linhas = cursor.fetchall()
conn.close()
return linhas
degrau_3_sqlite/test_degrau_3.py):"""
Testes Unitários para o Degrau 3 do Projeto 02 (SQLite Relacional com Foreign Key)
"""
import os
import unittest
from main import (
cadastrar_cliente,
calcular_saldo_cliente,
inicializar_tabelas,
listar_clientes_com_saldo,
registrar_movimentacao
)
BANCO_TESTE = "teste_fiado_degrau3.db"
class TestDegrau3SQLite(unittest.TestCase):
def setUp(self):
if os.path.exists(BANCO_TESTE):
os.remove(BANCO_TESTE)
inicializar_tabelas(caminho=BANCO_TESTE)
def tearDown(self):
if os.path.exists(BANCO_TESTE):
os.remove(BANCO_TESTE)
def test_relacionamento_e_calculo_saldo(self):
# 1. Cadastra cliente
c_id = cadastrar_cliente("Seu Raimundo do Bar", "99123-4567", caminho=BANCO_TESTE)
self.assertIsNotNone(c_id)
# 2. Registra compras e pagamentos
registrar_movimentacao(c_id, "COMPRA", 80.00, "Cerveja e fumo", caminho=BANCO_TESTE)
registrar_movimentacao(c_id, "COMPRA", 20.00, "Pacote de cafe", caminho=BANCO_TESTE)
registrar_movimentacao(c_id, "PAGAMENTO", 30.00, "Pagamento parcial", caminho=BANCO_TESTE)
# 3. Valida cálculo de saldo relacional (80 + 20 - 30 = 70)
saldo = calcular_saldo_cliente(c_id, caminho=BANCO_TESTE)
self.assertEqual(saldo, 70.00)
# 4. Listagem com JOIN
clientes = listar_clientes_com_saldo(caminho=BANCO_TESTE)
self.assertEqual(len(clientes), 1)
self.assertEqual(clientes[0][1], "Seu Raimundo do Bar")
self.assertEqual(clientes[0][3], 70.00)
if __name__ == "__main__":
unittest.main()
No quarto degrau, o estudante cria um painel web no navegador com Streamlit, permitindo cadastrar clientes na barra lateral, anotar compras fiado com seleção automática, registrar pagamentos e visualizar o extrato detalhado.
degrau_4_interface_streamlit/app.py):import sqlite3
import streamlit as st
BANCO_WEB = "fiado_web.db"
# Inicialização e funções SQL
def inicializar_banco():
conn = sqlite3.connect(BANCO_WEB)
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS clientes (id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL UNIQUE, telefone TEXT)")
cursor.execute("""
CREATE TABLE IF NOT EXISTS movimentacoes_fiado (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cliente_id INTEGER NOT NULL,
tipo TEXT NOT NULL CHECK(tipo IN ('COMPRA', 'PAGAMENTO')),
valor REAL NOT NULL,
descricao TEXT,
data_registro TEXT DEFAULT (datetime('now', 'localtime')),
FOREIGN KEY (cliente_id) REFERENCES clientes(id) ON DELETE CASCADE
)
""")
conn.commit()
conn.close()
def main():
inicializar_banco()
st.set_page_config(page_title="Caderno de Fiado da Mercearia", page_icon="📒", layout="wide")
st.title("📒 Caderno de Fiado da Mercearia / Padaria")
st.caption("Sistema de controle de crédito para comércios locais (Degrau 4: Web com Streamlit)")
# 1. Barra Lateral: Cadastro
st.sidebar.header("👤 Novo Cliente")
with st.sidebar.form("form_novo_cliente", clear_on_submit=True):
nome = st.text_input("Nome Completo do Cliente")
telefone = st.text_input("Telefone / WhatsApp")
if st.form_submit_button("Cadastrar Cliente"):
if nome.strip():
# Inserção no SQLite
pass
# 2. Métricas Financeiras em Destaque
col1, col2, col3 = st.columns(3)
col1.metric("💰 Total a Receber no Mês", "R$ 485,50")
col2.metric("👥 Clientes Cadastrados", "12")
col3.metric("⏳ Clientes com Débito Aberto", "5")
degrau_4_interface_streamlit/main.py):"""
Launcher para o Degrau 4 (Streamlit Web)
Verifica se o pacote streamlit está instalado e inicia a aplicação no navegador.
"""
import os
import subprocess
import sys
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
APP_FILE = os.path.join(BASE_DIR, "app.py")
def main():
print("\n========================================================")
print("🌐 INICIANDO CADERNO DE FIADO NA WEB (STREAMLIT)")
print("========================================================\n")
try:
import streamlit # noqa: F401
print("✅ Streamlit detectado! Abrindo a aplicacao no seu navegador padrao...\n")
cmd = [sys.executable, "-m", "streamlit", "run", APP_FILE]
subprocess.run(cmd, cwd=BASE_DIR)
except ImportError:
print("⚠️ O pacote 'streamlit' nao foi encontrado no ambiente Python atual.")
print("\nPara instalar e rodar, execute no seu terminal:")
print(" pip install streamlit")
print(f" streamlit run \"{APP_FILE}\"\n")
if __name__ == "__main__":
main()
main.py)O arquivo main.py na raiz do projeto funciona como um painel de controle: permite executar qualquer um dos 4 degraus (ou toda a suíte de testes) a partir de um único menu no terminal, sem precisar navegar manualmente entre as pastas.
main.py):"""
Hub Central / Launcher — Miniprojeto 02: Caderno de Fiado da Mercearia / Padaria
Permite executar de forma autoguiada qualquer um dos 4 degraus pedagógicos ou rodar a suíte de testes.
"""
import os
import subprocess
import sys
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def rodar_script(caminho_relativo):
caminho_absoluto = os.path.join(BASE_DIR, caminho_relativo)
diretorio = os.path.dirname(caminho_absoluto)
print(f"\n🚀 Executando: {caminho_relativo}...\n")
try:
subprocess.run([sys.executable, caminho_absoluto], cwd=diretorio, check=True)
except KeyboardInterrupt:
print("\n\n⏹️ Execucao interrompida pelo usuario.")
except Exception as e:
print(f"\n❌ Erro durante a execucao: {e}")
def rodar_todos_os_testes():
print("\n🧪 ========================================================")
print(" RODANDO A SUÍTE DE TESTES DOS DEGRAUS 1, 2 E 3")
print("========================================================\n")
testes = [
("Degrau 1 (Imperativo & JSON)", "degrau_1_imperativo_json/test_degrau_1.py"),
("Degrau 2 (Primeira Classe POO)", "degrau_2_primeira_classe/test_degrau_2.py"),
("Degrau 3 (Banco SQLite3 Relacional)", "degrau_3_sqlite/test_degrau_3.py"),
]
for nome, arq in testes:
print(f"▶️ Testando {nome}...")
caminho_abs = os.path.join(BASE_DIR, arq)
diretorio = os.path.dirname(caminho_abs)
res = subprocess.run([sys.executable, caminho_abs], cwd=diretorio)
if res.returncode == 0:
print(f"✅ {nome}: PASSOU!\n")
else:
print(f"❌ {nome}: FALHOU!\n")
def menu_hub():
while True:
print("\n========================================================")
print("🌱 MINIPROJETO 02: CADERNO DE FIADO (MERCEARIA / PADARIA)")
print("========================================================")
print("1. 🪜 Executar Degrau 1 (Imperativo & Arquivo .json)")
print("2. 🪜 Executar Degrau 2 (Primeira Classe POO: class Cliente)")
print("3. 🪜 Executar Degrau 3 (Banco de Dados SQLite3 Relacional)")
print("4. 🪜 Executar Degrau 4 (Interface Web com Streamlit)")
print("5. 🧪 Rodar Todos os Testes Automatizados (Degraus 1, 2 e 3)")
print("6. 🚪 Sair")
print("========================================================")
opcao = input("Escolha o degrau que deseja executar (1-6): ").strip()
if opcao == "1":
rodar_script("degrau_1_imperativo_json/main.py")
elif opcao == "2":
rodar_script("degrau_2_primeira_classe/main.py")
elif opcao == "3":
rodar_script("degrau_3_sqlite/main.py")
elif opcao == "4":
rodar_script("degrau_4_interface_streamlit/main.py")
elif opcao == "5":
rodar_todos_os_testes()
elif opcao == "6":
print("👋 Encerrando o launcher. Bons estudos!")
break
else:
print("❌ Opcao invalida. Digite de 1 a 6.")
if __name__ == "__main__":
menu_hub()
Cada degrau possui sua suíte de testes unitários com isolamento completo de banco de dados temporário:
# Executar a suíte de testes do Miniprojeto 02:
python -c "import subprocess, sys; [subprocess.run([sys.executable, p], check=True) for p in ['catalogo_projetos/sistemas_python_intro_02_fiado_padaria/degrau_1_imperativo_json/test_degrau_1.py', 'catalogo_projetos/sistemas_python_intro_02_fiado_padaria/degrau_2_primeira_classe/test_degrau_2.py', 'catalogo_projetos/sistemas_python_intro_02_fiado_padaria/degrau_3_sqlite/test_degrau_3.py']]"
# Ou, de forma ainda mais simples, use o Hub Central (opção 5 do menu):
python main.py
Resultado obtido:
Ran 2 tests in 0.008s OK (Degrau 1)
Ran 2 tests in 0.004s OK (Degrau 2)
Ran 1 test in 0.063s OK (Degrau 3)
A imagem abaixo ilustra a esteira evolutiva dos 4 degraus didáticos do miniprojeto:
flowchart TD
subgraph S1["🪜 Degrau 1: Script Imperativo"]
A1["Menu Terminal while"] --> A2["Arquivo de Texto/JSON"]
end
subgraph S2["🪜 Degrau 2: Orientação a Objetos"]
B1["Modelo de Domínio POO"] --> B2["Encapsulamento de Regras"]
end
subgraph S3["🪜 Degrau 3: Banco Relacional"]
C1["SQLite3 Local"] --> C2["Transações ACID & Agregações"]
end
subgraph S4["🪜 Degrau 4: Interface Visual"]
D1["Aplicação Desktop / Web"] --> D2["UX Interativa Final"]
end
S1 --> S2
S2 --> S3
S3 --> S4
style S1 fill:#fffde7,stroke:#fbc02d,stroke-width:1.5px
style S2 fill:#e3f2fd,stroke:#1976d2,stroke-width:1.5px
style S3 fill:#e8f5e9,stroke:#388e3c,stroke-width:1.5px
style S4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1.5px
| ID | Persona | História de Usuário | Critério de Aceite | Prioridade |
|---|---|---|---|---|
| US-01 | Dono da Mercearia | Desejo registrar compras a prazo de clientes em formato JSON aninhado | Armazenar dicionários com cliente, data e valor no fiado.json | Alta |
| US-02 | Comerciante | Desejo modelar meus clientes com saldo devedor consolidado em objeto POO | Classe Cliente com métodos adicionar_compra e registrar_pagamento | Alta |
| US-03 | Operador de Caixa | Desejo integridade referencial entre clientes e movimentações de crédito | Tabelas clientes e movimentacoes com Foreign Key no SQLite3 | Alta |
| US-04 | Gestor do Ponto de Venda | Desejo visualizar painel web com métricas de fiado total e extratos | Dashboard Streamlit interativo com filtros e tabelas dinâmicas | Alta |
| US-05 | Auditor de Negócio | Desejo validar que os saldos devedores são calculados com exatidão matemática | Testes unitários garantindo que pagamentos nunca gerem saldo negativo indevido | Média |
| ID | Descrição do Requisito | Criticidade |
|---|---|---|
| RF-01 | Serialização e deserialização atômica de cadastros e dívidas em arquivo JSON | Alta |
| RF-02 | Modelagem da classe Cliente com controle de limite e histórico de operações | Alta |
| RF-03 | Relacionamento 1:N no SQLite com consultas de agregação SUM() e GROUP BY | Alta |
| RF-04 | Interface web Streamlit com métricas de dívida ativa, formulários e gráficos | Alta |
| RF-05 | Módulo de testes automatizados com banco em memória (:memory:) isolado | Média |
| ID | Especificação Arquitetural | Criticidade |
|---|---|---|
| RNF-01 | Processamento determinístico de valores monetários sem perda por ponto flutuante | Alta |
| RNF-02 | Tempo de resposta de consultas agregadas inferior a 200ms | Alta |
| RNF-03 | Validação de entrada contra nomes em branco e valores negativos de dívida | Média |
| RNF-04 | Design responsivo na visualização web em desktops e tablets do balcão | Média |
| RNF-05 | Isolamento modular por degrau com launcher central unificado | Alta |
| User Story | Requisito Funcional | Requisito Não-Funcional | Módulo / Artefato de Código | Status de Homologação |
|---|---|---|---|---|
| US-01 | RF-01 |
RNF-01, RNF-03 |
degrau_1_imperativo_json/main.py |
Testado |
| US-02 | RF-02 |
RNF-01, RNF-05 |
degrau_2_primeira_classe/main.py |
Testado |
| US-03 | RF-03 |
RNF-02, RNF-05 |
degrau_3_sqlite/main.py |
Testado |
| US-04 | RF-04 |
RNF-02, RNF-04 |
degrau_4_interface_streamlit/app.py |
Testado |
| US-05 | RF-05 |
RNF-01, RNF-03 |
main.py & test_degrau_*.py |
Testado |
| Checkpoint | Escopo da Validação | Artefato Verificado | Status |
|---|---|---|---|
| CP-01 | Script Imperativo & Persistência Inicial | degrau_1_* |
PASS ✅ |
| CP-02 | Modelagem Orientada a Objetos | degrau_2_* |
PASS ✅ |
| CP-03 | Integridade Relacional no SQLite | degrau_3_* |
PASS ✅ |
| CP-04 | Interface Visual & Usabilidade | degrau_4_* |
PASS ✅ |
| CP-05 | Hub Launcher Central & Suíte de Testes | main.py + test_*.py |
PASS ✅ |