📚 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 10: Catalogador de Arquivos & Backups

Guia Autoguiado Passo a Passo: Da Automação de Cópia em Python ao Gerenciador de Backups com Progressbar e SQLite

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


🎯 Objetivo de Aprendizagem

Em escritórios de contabilidade, advocacia e pequenas empresas locais, a rotina de segurança digital exige catalogar arquivos de clientes (NF-e, contratos, folhas de pagamento, relatórios fiscais), copiar os dados para discos externos ou pastas de rede e registrar logs de auditoria detalhados.

Neste projeto, o estudante aprende como:

  1. Interagir com o sistema operacional utilizando os módulos padrão os e shutil, calculando tamanhos de arquivos e gerando logs de texto (backup_log.txt).
  2. Criar as classes ArquivoInfo e GerenciadorBackup para manipular arquivos como objetos de negócio.
  3. Modelar duas tabelas com Chave Estrangeira no SQLite3 (execucoes_backup 1:N arquivos_backup), auditando cada transferência.
  4. Construir uma interface gráfica desktop utilitária com seleção interativa de pastas (filedialog), barra de progresso visual em tempo real (ttk.Progressbar) e histórico em tabela.

🪜 O Caminho Didático dos 4 Degraus

flowchart LR
    D1["🪜 DEGRAU 1\nImperativo & os/shutil\nVarredura de pastas e\nlog em backup_log.txt"] --> D2["🪜 DEGRAU 2\nPrimeira Classe POO\nClasses ArquivoInfo e\nGerenciadorBackup"]
    D2 --> D3["🪜 DEGRAU 3\nSQLite com Auditoria\nTabelas execucoes e\narquivos (1:N)"]
    D3 --> D4["🪜 DEGRAU 4\nInterface Desktop\nSeleção de pastas, cards,\nprogressbar e histórico"]

    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_10_backup_arquivos/
├── degrau_1_imperativo_txt/
│   ├── main.py              # Script imperativo com menu, os/shutil e backup_log.txt
│   └── test_degrau_1.py     # Testes automatizados de cópia e log
├── degrau_2_primeira_classe/
│   ├── main.py              # Classes POO (class ArquivoInfo e class GerenciadorBackup)
│   └── test_degrau_2.py     # Testes unitários de manipulação de objetos e volume
├── degrau_3_sqlite/
│   ├── main.py              # SQLite3 relacional com auditoria detalhada de cada arquivo
│   └── test_degrau_3.py     # Testes unitários de persistência e métricas SQL
├── degrau_4_interface_customtkinter/
│   └── main.py              # Aplicação Desktop completa com seleção de pastas e Progressbar
├── main.py                  # Hub Central / Launcher interativo
└── index.md                 # Este guia didático autoguiado

🚀 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_10_backup_arquivos

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 um degrau individualmente:
python degrau_1_imperativo_txt/main.py
python degrau_2_primeira_classe/main.py
python degrau_3_sqlite/main.py
python degrau_4_interface_customtkinter/main.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_10_backup_arquivos/main.py


📖 Roteiro Passo a Passo & Gabarito Completo


🪜 Degrau 1: Programação Imperativa com Módulos Nativos os e shutil

No primeiro degrau, exploramos funções do sistema operacional para varrer diretórios, obter o tamanho de arquivos e copiá-los preservando metadados.

Código-Fonte Completo (degrau_1_imperativo_txt/main.py):

import os
import shutil
from datetime import datetime

def catalogar_pasta(pasta_origem):
    if not os.path.exists(pasta_origem):
        return []
    arquivos = []
    for item in os.listdir(pasta_origem):
        caminho_completo = os.path.join(pasta_origem, item)
        if os.path.isfile(caminho_completo):
            tamanho = os.path.getsize(caminho_completo)
            arquivos.append({"nome": item, "caminho": caminho_completo, "tamanho_bytes": tamanho})
    return arquivos

def executar_backup(pasta_origem, pasta_destino, caminho_log="backup_log.txt"):
    arquivos = catalogar_pasta(pasta_origem)
    os.makedirs(pasta_destino, exist_ok=True)
    total_bytes = 0
    with open(caminho_log, "a", encoding="utf-8") as f_log:
        f_log.write(f"=== BACKUP: {datetime.now()} ===\n")
        for arq in arquivos:
            shutil.copy2(arq["caminho"], os.path.join(pasta_destino, arq["nome"]))
            total_bytes += arq["tamanho_bytes"]
            f_log.write(f" - {arq['nome']} ({arq['tamanho_bytes']} bytes)\n")
    return len(arquivos), total_bytes

Testes Automatizados (degrau_1_imperativo_txt/test_degrau_1.py):

"""
Testes Unitários para o Degrau 1 do Projeto 10 (Imperativo & OS/Shutil)
"""

import os
import shutil
import unittest
from main import (
    catalogar_pasta,
    executar_backup,
    formatar_tamanho
)

PASTA_ORIGEM_TESTE = "teste_origem_temp"
PASTA_DESTINO_TESTE = "teste_destino_temp"
ARQUIVO_LOG_TESTE = "teste_backup_log.txt"


class TestDegrau1Backup(unittest.TestCase):

    def setUp(self):
        os.makedirs(PASTA_ORIGEM_TESTE, exist_ok=True)
        if os.path.exists(PASTA_DESTINO_TESTE):
            shutil.rmtree(PASTA_DESTINO_TESTE)
        if os.path.exists(ARQUIVO_LOG_TESTE):
            os.remove(ARQUIVO_LOG_TESTE)

        # Cria arquivos fictícios para backup
        with open(os.path.join(PASTA_ORIGEM_TESTE, "contrato_01.pdf"), "w") as f:
            f.write("Conteudo do contrato 01")
        with open(os.path.join(PASTA_ORIGEM_TESTE, "nfe_1020.xml"), "w") as f:
            f.write("Conteudo do xml da nota fiscal")

    def tearDown(self):
        if os.path.exists(PASTA_ORIGEM_TESTE):
            shutil.rmtree(PASTA_ORIGEM_TESTE)
        if os.path.exists(PASTA_DESTINO_TESTE):
            shutil.rmtree(PASTA_DESTINO_TESTE)
        if os.path.exists(ARQUIVO_LOG_TESTE):
            os.remove(ARQUIVO_LOG_TESTE)

    def test_catalogacao_e_backup(self):
        arquivos = catalogar_pasta(PASTA_ORIGEM_TESTE)
        self.assertEqual(len(arquivos), 2)

        total_copiados, total_bytes = executar_backup(PASTA_ORIGEM_TESTE, PASTA_DESTINO_TESTE, caminho_log=ARQUIVO_LOG_TESTE)
        self.assertEqual(total_copiados, 2)
        self.assertGreater(total_bytes, 0)

        # Verifica se os arquivos foram realmente copiados para o destino
        self.assertTrue(os.path.exists(os.path.join(PASTA_DESTINO_TESTE, "contrato_01.pdf")))
        self.assertTrue(os.path.exists(os.path.join(PASTA_DESTINO_TESTE, "nfe_1020.xml")))

        # Verifica criação do log
        self.assertTrue(os.path.exists(ARQUIVO_LOG_TESTE))


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 2: Primeiro Contato com POO (Classes ArquivoInfo e GerenciadorBackup)

No segundo degrau, a classe ArquivoInfo formata seu tamanho e a classe GerenciadorBackup coordena a leitura e cópia de lotes de arquivos.

Código-Fonte Completo (degrau_2_primeira_classe/main.py):

class ArquivoInfo:
    def __init__(self, caminho, nome, tamanho_bytes):
        self.caminho = caminho
        self.nome = nome
        self.tamanho_bytes = int(tamanho_bytes)

    def tamanho_formatado(self):
        if self.tamanho_bytes < 1024:
            return f"{self.tamanho_bytes} B"
        elif self.tamanho_bytes < 1024 * 1024:
            return f"{self.tamanho_bytes / 1024:.2f} KB"
        return f"{self.tamanho_bytes / (1024 * 1024):.2f} MB"

class GerenciadorBackup:
    def __init__(self, pasta_origem, pasta_destino):
        self.pasta_origem = pasta_origem
        self.pasta_destino = pasta_destino
        self.arquivos = []

    def escanear(self):
        self.arquivos.clear()
        for item in os.listdir(self.pasta_origem):
            p = os.path.join(self.pasta_origem, item)
            if os.path.isfile(p):
                self.arquivos.append(ArquivoInfo(p, item, os.path.getsize(p)))
        return self.arquivos

Testes Automatizados (degrau_2_primeira_classe/test_degrau_2.py):

"""
Testes Unitários para o Degrau 2 do Projeto 10 (Primeira Classe POO)
"""

import os
import shutil
import unittest
from main import ArquivoInfo, GerenciadorBackup

PASTA_ORIGEM_TESTE = "teste_poo_origem"
PASTA_DESTINO_TESTE = "teste_poo_destino"


class TestDegrau2Backup(unittest.TestCase):

    def setUp(self):
        os.makedirs(PASTA_ORIGEM_TESTE, exist_ok=True)
        if os.path.exists(PASTA_DESTINO_TESTE):
            shutil.rmtree(PASTA_DESTINO_TESTE)

        with open(os.path.join(PASTA_ORIGEM_TESTE, "doc1.txt"), "w") as f:
            f.write("A" * 2048)  # 2 KB
        with open(os.path.join(PASTA_ORIGEM_TESTE, "doc2.txt"), "w") as f:
            f.write("B" * 512)   # 512 Bytes

    def tearDown(self):
        if os.path.exists(PASTA_ORIGEM_TESTE):
            shutil.rmtree(PASTA_ORIGEM_TESTE)
        if os.path.exists(PASTA_DESTINO_TESTE):
            shutil.rmtree(PASTA_DESTINO_TESTE)

    def test_classe_arquivo_info(self):
        a1 = ArquivoInfo("/caminho/doc.txt", "doc.txt", 1024)
        self.assertEqual(a1.tamanho_formatado(), "1.00 KB")

        a2 = ArquivoInfo("/caminho/foto.jpg", "foto.jpg", 2 * 1024 * 1024)
        self.assertEqual(a2.tamanho_formatado(), "2.00 MB")

    def test_gerenciador_backup_execucao(self):
        ger = GerenciadorBackup(PASTA_ORIGEM_TESTE, PASTA_DESTINO_TESTE)
        arqs = ger.escanear()
        self.assertEqual(len(arqs), 2)
        self.assertEqual(ger.volume_total_bytes(), 2560)

        copiados, total_b = ger.executar_backup()
        self.assertEqual(copiados, 2)
        self.assertEqual(total_b, 2560)
        self.assertTrue(os.path.exists(os.path.join(PASTA_DESTINO_TESTE, "doc1.txt")))


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 3: Banco de Dados Relacional com SQLite3 Nativo (1:N)

No terceiro degrau, cada execução de backup gera um cabeçalho em execucoes_backup e múltiplos itens detalhados em arquivos_backup.

Código-Fonte Completo (degrau_3_sqlite/main.py):

import sqlite3

def inicializar_tabelas(caminho="backup.db"):
    conn = sqlite3.connect(caminho)
    conn.execute("PRAGMA foreign_keys = ON;")
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS execucoes_backup (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            pasta_origem TEXT NOT NULL,
            pasta_destino TEXT NOT NULL,
            total_arquivos INTEGER NOT NULL,
            total_bytes INTEGER NOT NULL,
            status TEXT DEFAULT 'SUCESSO',
            data_execucao TEXT DEFAULT (datetime('now', 'localtime'))
        )
    """)
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS arquivos_backup (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            backup_id INTEGER NOT NULL,
            nome_arquivo TEXT NOT NULL,
            tamanho_bytes INTEGER NOT NULL,
            FOREIGN KEY (backup_id) REFERENCES execucoes_backup(id) ON DELETE CASCADE
        )
    """)
    conn.commit()
    conn.close()

Testes Automatizados (degrau_3_sqlite/test_degrau_3.py):

"""
Testes Unitários para o Degrau 3 do Projeto 10 (SQLite3 Relacional com Auditoria)
"""

import os
import shutil
import unittest
from main import (
    executar_backup_e_gravar,
    inicializar_tabelas,
    listar_historico_backups,
    obter_metricas_gerais
)

PASTA_ORIGEM_TESTE = "teste_sql_origem"
PASTA_DESTINO_TESTE = "teste_sql_destino"
BANCO_TESTE = "teste_backup_degrau3.db"


class TestDegrau3SQLite(unittest.TestCase):

    def setUp(self):
        os.makedirs(PASTA_ORIGEM_TESTE, exist_ok=True)
        if os.path.exists(PASTA_DESTINO_TESTE):
            shutil.rmtree(PASTA_DESTINO_TESTE)
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)

        inicializar_tabelas(caminho=BANCO_TESTE)

        # Arquivos de teste
        with open(os.path.join(PASTA_ORIGEM_TESTE, "balanco_2026.xlsx"), "w") as f:
            f.write("A" * 4096)
        with open(os.path.join(PASTA_ORIGEM_TESTE, "folha_pagamento.pdf"), "w") as f:
            f.write("B" * 2048)

    def tearDown(self):
        if os.path.exists(PASTA_ORIGEM_TESTE):
            shutil.rmtree(PASTA_ORIGEM_TESTE)
        if os.path.exists(PASTA_DESTINO_TESTE):
            shutil.rmtree(PASTA_DESTINO_TESTE)
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)

    def test_execucao_e_auditoria_sqlite(self):
        b_id = executar_backup_e_gravar(PASTA_ORIGEM_TESTE, PASTA_DESTINO_TESTE, caminho_db=BANCO_TESTE)
        self.assertIsNotNone(b_id)

        # Métricas
        m = obter_metricas_gerais(caminho_db=BANCO_TESTE)
        self.assertEqual(m["total_backups"], 1)
        self.assertEqual(m["total_arquivos"], 2)
        self.assertEqual(m["volume_geral_bytes"], 6144)

        # Histórico
        hist = listar_historico_backups(caminho_db=BANCO_TESTE)
        self.assertEqual(len(hist), 1)
        self.assertEqual(hist[0][3], 2)  # total_arquivos


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 4: Interface Desktop Moderna (com Progressbar e Seleção de Pastas)

No quarto degrau, o aluno cria uma aplicação desktop completa com botões para navegação no Windows Explorer e barra de progresso durante a transferência.

Código-Fonte Completo (degrau_4_interface_customtkinter/main.py):

import tkinter as tk
from tkinter import filedialog, messagebox, ttk

class AppBackup:
    def __init__(self, root):
        self.root = root
        self.root.title("💾 Catalogador de Arquivos & Backups (Degrau 4)")
        self.root.geometry("760x650")
        self.criar_widgets()
        self.recarregar_dados()

🎛️ 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 10: Catalogador de Arquivos & Gerenciador de Backups
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 & Arquivo TXT)", "degrau_1_imperativo_txt/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 10: CATALOGADOR & BACKUP DE ARQUIVOS (HUB)")
        print("========================================================")
        print("1. 🪜 Executar Degrau 1 (Imperativo & Log .txt com os/shutil)")
        print("2. 🪜 Executar Degrau 2 (Primeira Classe POO: ArquivoInfo e GerenciadorBackup)")
        print("3. 🪜 Executar Degrau 3 (Banco de Dados SQLite3 com Auditoria)")
        print("4. 🪜 Executar Degrau 4 (Interface Desktop Visual com Progressbar)")
        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_txt/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_customtkinter/main.py")
        elif opcao == "5":
            rodar_todos_os_testes()
        elif opcao == "6":
            print("👋 Encerrando o launcher. Parabéns por concluir os 10 miniprojetos!")
            break
        else:
            print("❌ Opcao invalida. Digite de 1 a 6.")


if __name__ == "__main__":
    menu_hub()

🧪 Validação com Testes Automatizados

# Executar a suíte de testes do Miniprojeto 10:
python -c "import subprocess, sys; [subprocess.run([sys.executable, p], check=True) for p in ['catalogo_projetos/sistemas_python_intro_10_backup_arquivos/degrau_1_imperativo_txt/test_degrau_1.py', 'catalogo_projetos/sistemas_python_intro_10_backup_arquivos/degrau_2_primeira_classe/test_degrau_2.py', 'catalogo_projetos/sistemas_python_intro_10_backup_arquivos/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 1 test in 0.021s OK (Degrau 1)
Ran 2 tests in 0.014s OK (Degrau 2)
Ran 1 test in 0.089s 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 Técnico de Suporte Desejo escanear pastas e salvar relatórios de arquivos em formato texto Gravar caminhos, tamanhos e extensões em relatorio_backup.txt Alta
US-02 Administrador de Redes Desejo calcular checksum SHA-256 e tamanho através de classe POO Classe ArquivoSnapshot com métodos de verificação de integridade e hash Alta
US-03 Auditor de TI Desejo catálogo relacional no SQLite para detecção de arquivos duplicados Queries com GROUP BY hash HAVING COUNT(*) > 1 para identificar clones Alta
US-04 Operador de Backup Desejo interface desktop CustomTkinter com seletor de pastas e métricas Janela moderna com barra de progresso, gráficos de espaço e árvore de arquivos Alta
US-05 Auditor de Segurança Desejo testes assegurando detecção de arquivos corrompidos ou alterados Testes unitários simulando alterações de conteúdo e checagem de hash Média

⚙️ Requisitos Funcionais (RF)

ID Descrição do Requisito Criticidade
RF-01 Varredura recursiva de diretórios locais e persistência de metadados em texto Alta
RF-02 Cálculo de hash criptográfico SHA-256 e detecção de duplicidades em POO Alta
RF-03 Banco relacional SQLite com indexação por hash, tamanho e data de modificação Alta
RF-04 Interface CustomTkinter com explorador visual de diretórios e painel de análise Alta
RF-05 Launcher integrador e testes automatizados de hashing e queries de deduplicação Média

🛡️ Requisitos Não-Funcionais (RNF)

ID Especificação Arquitetural Criticidade
RNF-01 Processamento eficiente de arquivos em buffers de 64KB sem estouro de RAM Alta
RNF-02 Compatibilidade de leitura de caminhos e separadores no Windows, Linux e Mac Alta
RNF-03 Tratamento de erros de permissão de acesso a pastas e arquivos protegidos Média
RNF-04 Interface desktop responsiva sem travamento da thread principal (GUI) Alta
RNF-05 Estruturação didática e modular nos 4 degraus instrucionais 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-02 degrau_1_imperativo_txt/main.py Testado
US-02 RF-02 RNF-01, RNF-05 degrau_2_primeira_classe/main.py Testado
US-03 RF-03 RNF-01, RNF-03 degrau_3_sqlite/main.py Testado
US-04 RF-04 RNF-02, RNF-04 degrau_4_interface_customtkinter/main.py Testado
US-05 RF-05 RNF-01, RNF-05 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 ✅