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

🌱 Miniprojeto 02: Caderno de Fiado da Mercearia / Padaria

Guia Autoguiado Passo a Passo: De Dicionários JSON à Aplicação Web com SQLite e Streamlit

Trilha de Especialização — Nível Introdutório (Curso 25: Sistemas Desktop com Python)


🎯 Objetivo de Aprendizagem

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:

  1. Estruturar dados de clientes e dívidas em arquivos .json aninhados com json.dump() e json.load().
  2. Criar a classe Cliente com métodos para adicionar compras e abater pagamentos, organizando a lógica de negócio.
  3. Modelar duas tabelas relacionais com Chave Estrangeira no SQLite (clientes e movimentacoes_fiado) e calcular saldos dinâmicos com SUM().
  4. Construir uma interface Web amigável e moderna com Streamlit, exibindo métricas financeiras, gráficos de saldo e extratos detalhados sem escrever uma única linha de HTML ou CSS.

🪜 O Caminho Didático dos 4 Degraus

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

📂 Estrutura de Pastas e Arquivos

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

📦 Requisitos & Ambiente

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 --version não funcionar no seu terminal, tente python3 --version. Em alguns sistemas o executável do Python 3 é registrado apenas como python3 (o mesmo vale para pip → pip3).

Para conferir se o Streamlit já está instalado e qual versão, sem precisar abrir o app:

pip show streamlit

Se aparecer WARNING: Package(s) not found, é porque ainda falta instalar — rode pip install streamlit.

[!NOTE] Você não precisa instalar nada para rodar os Degraus 1, 2 e 3 (terminal + JSON + SQLite). O pip install streamlit só é necessário quando for rodar o Degrau 4 (streamlit run degrau_4_interface_streamlit/app.py). O launcher degrau_4_interface_streamlit/main.py inclusive 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).


🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No VS Code / PyCharm, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd sistemas_python_intro_02_fiado_padaria

2. Execute o Hub Central ou um Degrau Específico

# 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


📖 Roteiro Passo a Passo & Gabarito Completo


🪜 Degrau 1: Programação Imperativa com Arquivo JSON (.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.

🆕 Novidades deste degrau (em relação ao Miniprojeto 01)

Código-Fonte Completo (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()

🔍 Entendendo o código, função por função

🖥️ Exemplo de Execução (o que você verá no terminal)

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

[!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 um for simples.

Testes Automatizados (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()

🪜 Degrau 2: Primeiro Contato com POO (Classe Cliente)

No segundo degrau, transformamos o cliente em um objeto da classe Cliente, encapsulando os métodos adicionar_compra() e pagar_conta().

Código-Fonte Completo (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)
        )

Testes Automatizados (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()

🪜 Degrau 3: Banco de Dados SQLite3 com Duas Tabelas Relacionais (1:N)

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

Código-Fonte Completo (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

Testes Automatizados (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()

🪜 Degrau 4: Interface Web Moderna em Python com Streamlit

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.

Código-Fonte Completo (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")

Launcher do Servidor Web (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()

🎛️ Hub Central: Launcher Interativo (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.

Código-Fonte Completo (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()

🧪 Validação com Testes Automatizados

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)


🏛️ Arquitetura Visual & Engenharia do Sistema

A imagem abaixo ilustra a esteira evolutiva dos 4 degraus didáticos do miniprojeto:

Arquitetura da Aplicação em 4 Degraus

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

🎯 Engenharia de Requisitos & Modelagem Formal

👤 User Stories (Histórias de Usuário)

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

⚙️ Requisitos Funcionais (RF)

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

🛡️ Requisitos Não-Funcionais (RNF)

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

🔗 Matriz de Rastreabilidade (RTM)

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

🏁 Checkpoints de Qualidade & Homologação

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 ✅