📚 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 05 (Código Completo em 4 Degraus)
Em bibliotecas escolares, comunitárias ou de igrejas em pequenas cidades, a gestão de quais livros estão na estante e quais estão emprestados é essencial para evitar o extravio de exemplares.
Neste projeto, o estudante aprende como:
.txt delimitados.Livro e Biblioteca para encapsular regras de empréstimo (bloqueio quando disponíveis == 0) e devolução.livros 1:N emprestimos) executando transações atômicas de saída e retorno de exemplares.ttk.Notebook), permitindo gerenciar o acervo, realizar empréstimos via combobox e registrar devoluções com um clique.flowchart LR
D1["🪜 DEGRAU 1\nImperativo & .txt\nMenu while + livros.txt\ncom controle de saldo"] --> D2["🪜 DEGRAU 2\nPrimeira Classe POO\nclass Livro &\nclass Biblioteca"]
D2 --> D3["🪜 DEGRAU 3\nSQLite Relacional (1:N)\nTabelas livros e\nemprestimos com FK"]
D3 --> D4["🪜 DEGRAU 4\nInterface Desktop\nAbas ttk.Notebook:\nAcervo, Empréstimo e Devolução"]
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_05_biblioteca_livros/
├── degrau_1_imperativo_txt/
│ ├── main.py # Script imperativo com menu e arquivo livros.txt
│ └── test_degrau_1.py # Testes automatizados de empréstimo e devolução
├── degrau_2_primeira_classe/
│ ├── main.py # Classes POO (class Livro e class Biblioteca)
│ └── test_degrau_2.py # Testes unitários de regras de negócio
├── degrau_3_sqlite/
│ ├── main.py # SQLite3 relacional: 2 tabelas com chave estrangeira (1:N)
│ └── test_degrau_3.py # Testes unitários de transações e JOIN
├── degrau_4_interface_tkinter/
│ └── main.py # Aplicação Desktop com abas (Acervo, Novo Empréstimo, Devoluções)
├── main.py # Hub Central / Launcher interativo
└── index.md # Este guia didático autoguiado
No VS Code / PyCharm, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd sistemas_python_intro_05_biblioteca_livros
# 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_tkinter/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_05_biblioteca_livros/main.py
.txt)No primeiro degrau, gravamos cada obra no arquivo livros.txt no formato titulo,autor,totais,disponiveis.
degrau_1_imperativo_txt/main.py):import os
ARQUIVO_PADRAO = "livros.txt"
def carregar_livros(caminho=ARQUIVO_PADRAO):
livros = []
if not os.path.exists(caminho):
return livros
with open(caminho, "r", encoding="utf-8") as f:
for linha in f:
partes = linha.strip().split(",")
if len(partes) >= 4:
livros.append({"titulo": partes[0], "autor": partes[1], "totais": int(partes[2]), "disponiveis": int(partes[3])})
return livros
def salvar_livros(livros, caminho=ARQUIVO_PADRAO):
with open(caminho, "w", encoding="utf-8") as f:
for l in livros:
f.write(f"{l['titulo']},{l['autor']},{l['totais']},{l['disponiveis']}\n")
def emprestar_livro(livros, titulo):
for l in livros:
if l["titulo"].lower() == titulo.lower():
if l["disponiveis"] <= 0:
print(f"[ERRO] Todos os {l['totais']} exemplares estao emprestados!")
return False
l["disponiveis"] -= 1
print(f"[OK] Emprestimo realizado! Disponiveis: {l['disponiveis']}/{l['totais']}.")
return True
return False
def devolver_livro(livros, titulo):
for l in livros:
if l["titulo"].lower() == titulo.lower():
if l["disponiveis"] >= l["totais"]:
return False
l["disponiveis"] += 1
return True
return False
degrau_1_imperativo_txt/test_degrau_1.py):"""
Testes Unitários para o Degrau 1 do Projeto 05 (Imperativo & Arquivo TXT)
"""
import os
import unittest
from main import (
cadastrar_livro,
carregar_livros,
devolver_livro,
emprestar_livro,
salvar_livros
)
ARQUIVO_TESTE = "teste_livros.txt"
class TestDegrau1Biblioteca(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_e_emprestar(self):
livros = []
cadastrar_livro(livros, "Dom Casmurro", "Machado de Assis", 2)
self.assertEqual(len(livros), 1)
self.assertEqual(livros[0]["disponiveis"], 2)
# Empréstimo 1
emp1 = emprestar_livro(livros, "Dom Casmurro")
self.assertTrue(emp1)
self.assertEqual(livros[0]["disponiveis"], 1)
# Empréstimo 2
emp2 = emprestar_livro(livros, "Dom Casmurro")
self.assertTrue(emp2)
self.assertEqual(livros[0]["disponiveis"], 0)
# Empréstimo 3 (Esgotado)
emp3 = emprestar_livro(livros, "Dom Casmurro")
self.assertFalse(emp3)
self.assertEqual(livros[0]["disponiveis"], 0)
# Devolução
dev = devolver_livro(livros, "Dom Casmurro")
self.assertTrue(dev)
self.assertEqual(livros[0]["disponiveis"], 1)
def test_persistencia_arquivo_txt(self):
livros = []
cadastrar_livro(livros, "O Alquimista", "Paulo Coelho", 5)
salvar_livros(livros, caminho=ARQUIVO_TESTE)
carregados = carregar_livros(caminho=ARQUIVO_TESTE)
self.assertEqual(len(carregados), 1)
self.assertEqual(carregados[0]["titulo"], "O Alquimista")
self.assertEqual(carregados[0]["totais"], 5)
if __name__ == "__main__":
unittest.main()
Livro e Biblioteca)No segundo degrau, a classe Livro encapsula o estado do exemplar e seus comportamentos (emprestar(), devolver()).
degrau_2_primeira_classe/main.py):class Livro:
def __init__(self, titulo, autor, totais, disponiveis=None):
self.titulo = titulo.strip()
self.autor = autor.strip() if autor else "Autor Desconhecido"
self.totais = int(totais)
self.disponiveis = int(disponiveis) if disponiveis is not None else int(totais)
def esta_disponivel(self):
return self.disponiveis > 0
def emprestar(self):
if self.disponiveis <= 0:
return False, "Todos os exemplares estão emprestados."
self.disponiveis -= 1
return True, f"Empréstimo realizado! Restam {self.disponiveis}/{self.totais}."
def devolver(self):
if self.disponiveis >= self.totais:
return False, "Todos os exemplares já se encontram na biblioteca."
self.disponiveis += 1
return True, f"Devolução confirmada! Disponíveis: {self.disponiveis}/{self.totais}."
degrau_2_primeira_classe/test_degrau_2.py):"""
Testes Unitários para o Degrau 2 do Projeto 05 (Primeira Classe POO)
"""
import os
import unittest
from main import Biblioteca, Livro
ARQUIVO_TESTE = "teste_livros_poo.txt"
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_livro_metodos(self):
l = Livro("Vidas Secas", "Graciliano Ramos", 1)
self.assertTrue(l.esta_disponivel())
# Empresta o único exemplar
sucesso, _ = l.emprestar()
self.assertTrue(sucesso)
self.assertFalse(l.esta_disponivel())
# Tenta emprestar novamente
falha, _ = l.emprestar()
self.assertFalse(falha)
# Devolve
dev, _ = l.devolver()
self.assertTrue(dev)
self.assertTrue(l.esta_disponivel())
def test_biblioteca_persistencia(self):
bib1 = Biblioteca()
bib1.adicionar(Livro("Capitaes da Areia", "Jorge Amado", 3))
bib1.salvar(caminho=ARQUIVO_TESTE)
bib2 = Biblioteca()
bib2.carregar(caminho=ARQUIVO_TESTE)
self.assertEqual(len(bib2.acervo), 1)
self.assertEqual(bib2.acervo[0].titulo, "Capitaes da Areia")
self.assertEqual(bib2.acervo[0].disponiveis, 3)
if __name__ == "__main__":
unittest.main()
No terceiro degrau, criamos as tabelas livros e emprestimos com FOREIGN KEY, realizando consultas com JOIN e baixas transacionais.
degrau_3_sqlite/main.py):import sqlite3
BANCO_PADRAO = "biblioteca.db"
def inicializar_tabelas(caminho=BANCO_PADRAO):
conn = sqlite3.connect(caminho)
conn.execute("PRAGMA foreign_keys = ON;")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS livros (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL UNIQUE,
autor TEXT,
total_exemplares INTEGER NOT NULL,
disponiveis INTEGER NOT NULL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS emprestimos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
livro_id INTEGER NOT NULL,
nome_leitor TEXT NOT NULL,
data_emprestimo TEXT DEFAULT (date('now', 'localtime')),
status TEXT DEFAULT 'ATIVO' CHECK(status IN ('ATIVO', 'DEVOLVIDO')),
FOREIGN KEY (livro_id) REFERENCES livros(id) ON DELETE CASCADE
)
""")
conn.commit()
conn.close()
degrau_3_sqlite/test_degrau_3.py):"""
Testes Unitários para o Degrau 3 do Projeto 05 (SQLite3 Relacional com Foreign Key)
"""
import os
import unittest
from main import (
cadastrar_livro,
inicializar_tabelas,
listar_acervo,
listar_emprestimos_ativos,
realizar_emprestimo,
registrar_devolucao
)
BANCO_TESTE = "teste_biblioteca_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_fluxo_emprestimo_e_devolucao(self):
# 1. Cadastra livro com 1 exemplar
livro_id = cadastrar_livro("Grande Sertao: Veredas", "Guimaraes Rosa", 1, caminho=BANCO_TESTE)
self.assertIsNotNone(livro_id)
# 2. Empresta o exemplar
emp_ok = realizar_emprestimo(livro_id, "Maria Clara", caminho=BANCO_TESTE)
self.assertTrue(emp_ok)
# 3. Verifica acervo zerado
acervo = listar_acervo(caminho=BANCO_TESTE)
self.assertEqual(acervo[0][4], 0) # disponiveis = 0
# 4. Tenta emprestar novamente (deve falhar)
emp_bloqueado = realizar_emprestimo(livro_id, "Joao Pedro", caminho=BANCO_TESTE)
self.assertFalse(emp_bloqueado)
# 5. Lista empréstimos ativos com JOIN
ativos = listar_emprestimos_ativos(caminho=BANCO_TESTE)
self.assertEqual(len(ativos), 1)
emp_id = ativos[0][0]
# 6. Devolve o livro
dev_ok = registrar_devolucao(emp_id, caminho=BANCO_TESTE)
self.assertTrue(dev_ok)
# 7. Verifica acervo restaurado
acervo_depois = listar_acervo(caminho=BANCO_TESTE)
self.assertEqual(acervo_depois[0][4], 1)
self.assertEqual(len(listar_emprestimos_ativos(caminho=BANCO_TESTE)), 0)
if __name__ == "__main__":
unittest.main()
ttk.Notebook)No quarto degrau, o aluno constrói uma aplicação desktop moderna dividida em 3 abas temáticas com o widget ttk.Notebook.
degrau_4_interface_tkinter/main.py):import tkinter as tk
from tkinter import ttk
class AppBiblioteca:
def __init__(self, root):
self.root = root
self.root.title("📚 Biblioteca Comunitária (Degrau 4)")
self.root.geometry("680x560")
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill="both", expand=True, padx=15, pady=10)
# Abas
self.tab_acervo = tk.Frame(self.notebook, bg="#FFFFFF")
self.notebook.add(self.tab_acervo, text=" 📖 Acervo de Livros ")
self.tab_emprestimo = tk.Frame(self.notebook, bg="#FFFFFF")
self.notebook.add(self.tab_emprestimo, text=" 🤝 Novo Empréstimo ")
self.tab_devolucao = tk.Frame(self.notebook, bg="#FFFFFF")
self.notebook.add(self.tab_devolucao, text=" ↩️ Devoluções / Ativos ")
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 05: Empréstimo de Livros da Biblioteca Comunitária
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 05: BIBLIOTECA COMUNITÁRIA (HUB)")
print("========================================================")
print("1. 🪜 Executar Degrau 1 (Imperativo & Arquivo .txt)")
print("2. 🪜 Executar Degrau 2 (Primeira Classe POO: class Livro)")
print("3. 🪜 Executar Degrau 3 (Banco de Dados SQLite3 Relacional)")
print("4. 🪜 Executar Degrau 4 (Interface Desktop com Abas Tkinter)")
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_tkinter/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()
# Executar a suíte de testes do Miniprojeto 05:
python -c "import subprocess, sys; [subprocess.run([sys.executable, p], check=True) for p in ['catalogo_projetos/sistemas_python_intro_05_biblioteca_livros/degrau_1_imperativo_txt/test_degrau_1.py', 'catalogo_projetos/sistemas_python_intro_05_biblioteca_livros/degrau_2_primeira_classe/test_degrau_2.py', 'catalogo_projetos/sistemas_python_intro_05_biblioteca_livros/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.004s OK (Degrau 1)
Ran 2 tests in 0.005s OK (Degrau 2)
Ran 1 test in 0.068s 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 | Bibliotecário Voluntário | Desejo catalogar títulos e autores em arquivo texto simples | Persistir acervo em livros.txt com status de disponibilidade | Alta |
| US-02 | Atendente da Biblioteca | Desejo gerenciar operações de empréstimo e devolução em classe POO | Classe Livro com métodos emprestar() e devolver() com validações | Alta |
| US-03 | Gestor do Acervo | Desejo controlar datas limite e leitores com SQLite relacional | Tabela de empréstimos com histórico de devoluções no SQLite3 | Alta |
| US-04 | Operador do Balcão | Desejo janela desktop com busca rápida por título e botões de ação | Interface Tkinter com campo de busca dinâmica e tabela de acervo | Alta |
| US-05 | Auditor de Acervo | Desejo garantir que um livro já emprestado não possa ser emprestado de novo | Testes unitários impedindo empréstimos duplicados do mesmo exemplar | Média |
| ID | Descrição do Requisito | Criticidade |
|---|---|---|
| RF-01 | Cadastro e recuperação de obras literárias em arquivo texto delimitado | Alta |
| RF-02 | Encapsulamento de estado (Disponível/Emprestado) na classe de domínio Livro | Alta |
| RF-03 | Schema SQLite para controle de obras, leitores e histórico de movimentações | Alta |
| RF-04 | Aplicação gráfica Tkinter com formulário de empréstimo e painel de status | Alta |
| RF-05 | Launcher unificado com execução de suíte de testes de integridade | Média |
| ID | Especificação Arquitetural | Criticidade |
|---|---|---|
| RNF-01 | Biblioteca padrão sem dependências de pacotes externos do pip | Alta |
| RNF-02 | Operações de busca em catálogo com latência inferior a 100ms | Alta |
| RNF-03 | Mensagens de erro instrutivas e amigáveis ao usuário não técnico | Média |
| RNF-04 | Conformidade com padrões PEP 8 de estilo e tipagem em Python | Média |
| RNF-05 | Execução consistente em qualquer sistema operacional suportado | 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-04 |
degrau_1_imperativo_txt/main.py |
Testado |
| US-02 | RF-02 |
RNF-01, RNF-03 |
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-01, RNF-03 |
degrau_4_interface_tkinter/main.py |
Testado |
| US-05 | RF-05 |
RNF-01, RNF-05 |
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 ✅ |