Ficha 02 · Módulos, JSON, APIs e testes
- Criar módulos
- Ler/escrever JSON e CSV
- Chamar APIs
- Escrever testes pytest
Parte I · Módulos
Exercício 1 · Organizar projecto (10 pts)
Tens um ficheiro de 800 linhas com tudo dentro. Propõe estrutura para dividir em módulos:
loja.py (800 linhas)
loja/
├── __init__.py
├── main.py # entry point
├── modelos.py # classes Cliente, Produto, Encomenda
├── persistencia.py # ler/gravar ficheiros JSON
├── menu.py # interacção com utilizador
├── relatorios.py # estatísticas, gráficos
└── utils.py # funções genéricas
Ou estrutura ligeiramente diferente conforme natureza dos 800 linhas. Princípios: - Cada módulo com responsabilidade clara. - Módulos com 50-300 linhas (não mais). - Imports unidireccionais (modelos não importa persistencia, persistencia importa modelos).
Exercício 2 · pip e virtualenv (15 pts)
Descreve passos para começar um projecto Python novo com:
- Ambiente virtual isolado.
- 3 dependências (requests, pytest, rich).
- Ficheiro requirements.txt.
# 1. Criar pasta do projecto
mkdir meu-projecto && cd meu-projecto
# 2. Criar virtualenv
python -m venv venv
# 3. Activar
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# 4. Instalar dependências
pip install requests pytest rich
# 5. Guardar dependências
pip freeze > requirements.txt
# Mais tarde, outro programador clona o repo e faz:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
venv/ deve estar no .gitignore — não se versiona o ambiente, apenas requirements.txt.
Parte II · JSON e CSV
Exercício 3 · Manipular JSON (15 pts)
Dado um ficheiro alunos.json:
[
{"nome": "Ana", "notas": [15, 16, 14]},
{"nome": "João", "notas": [10, 12, 13]}
]
Escreve programa que:
a) Lê o ficheiro.
b) Calcula a média de cada aluno.
c) Adiciona campo media a cada aluno.
d) Grava de volta.
import json
with open("alunos.json", encoding="utf-8") as f:
alunos = json.load(f)
for aluno in alunos:
notas = aluno["notas"]
aluno["media"] = round(sum(notas) / len(notas), 2) if notas else 0
with open("alunos.json", "w", encoding="utf-8") as f:
json.dump(alunos, f, indent=2, ensure_ascii=False)
print("Médias calculadas e gravadas.")
Exercício 4 · CSV → JSON (10 pts)
Dado vendas.csv:
data,produto,qty,preco
2026-05-01,Caneta,10,1.50
2026-05-02,Caderno,5,3.20
Converte para vendas.json com mesma estrutura. Calcula campo extra total (qty × preco).
import csv
import json
vendas = []
with open("vendas.csv", encoding="utf-8") as f:
reader = csv.DictReader(f)
for linha in reader:
linha["qty"] = int(linha["qty"])
linha["preco"] = float(linha["preco"])
linha["total"] = linha["qty"] * linha["preco"]
vendas.append(linha)
with open("vendas.json", "w", encoding="utf-8") as f:
json.dump(vendas, f, indent=2, ensure_ascii=False)
Parte III · APIs
Exercício 5 · Consumir API (15 pts)
Usa a API pública https://api.github.com/users/{username} para mostrar info de um utilizador GitHub:
a) Lê username do input.
b) Faz GET à API.
c) Mostra name, bio, public_repos, followers.
d) Trata erros (utilizador não existe, sem internet).
import requests
def info_github(username):
url = f"https://api.github.com/users/{username}"
try:
r = requests.get(url, timeout=5)
except requests.RequestException as e:
print(f"Erro de rede: {e}")
return
if r.status_code == 404:
print(f"Utilizador '{username}' não encontrado.")
return
if r.status_code != 200:
print(f"Erro {r.status_code}")
return
dados = r.json()
print(f"Nome: {dados.get('name', '(sem nome)')}")
print(f"Bio: {dados.get('bio', '(sem bio)')}")
print(f"Repos: {dados['public_repos']}")
print(f"Seguid.: {dados['followers']}")
if __name__ == "__main__":
user = input("Username GitHub: ")
info_github(user)
Exercício 6 · API com paginação (15 pts)
A API GitHub limita resultados por página (30 por defeito). Para listar todos os repos de um utilizador, é preciso paginar.
Implementa função que devolve todos os repos, iterando páginas até esgotar:
import requests
def todos_repos(username):
repos = []
pagina = 1
while True:
url = f"https://api.github.com/users/{username}/repos"
params = {"page": pagina, "per_page": 100}
r = requests.get(url, params=params, timeout=5)
if r.status_code != 200:
break
lote = r.json()
if not lote:
break # acabou
repos.extend(lote)
pagina += 1
if len(lote) < 100:
break # última página
return repos
repos = todos_repos("torvalds")
print(f"Total: {len(repos)} repos")
for r in repos[:5]:
print(f" {r['name']}: {r['stargazers_count']}⭐")
Parte IV · Testes
Exercício 7 · Escrever testes (20 pts)
Para a seguinte função, escreve 4 testes pytest cobrindo casos normais e erros:
def media_ponderada(notas, pesos):
if len(notas) != len(pesos):
raise ValueError("Notas e pesos têm de ter mesmo tamanho")
if sum(pesos) == 0:
raise ValueError("Soma de pesos não pode ser 0")
soma = sum(n * p for n, p in zip(notas, pesos))
return soma / sum(pesos)
import pytest
from media import media_ponderada
def test_media_normal():
assert media_ponderada([10, 20], [1, 1]) == 15.0
def test_media_pesos_diferentes():
# 10*1 + 20*3 = 70; soma_pesos = 4; média = 17.5
assert media_ponderada([10, 20], [1, 3]) == 17.5
def test_listas_tamanhos_diferentes():
with pytest.raises(ValueError, match="mesmo tamanho"):
media_ponderada([10, 20], [1])
def test_pesos_soma_zero():
with pytest.raises(ValueError, match="não pode ser 0"):
media_ponderada([10, 20], [0, 0])
# Bónus: parametrizado
@pytest.mark.parametrize("notas,pesos,esperado", [
([10], [1], 10.0),
([10, 20, 30], [1, 1, 1], 20.0),
([20, 20], [1, 1], 20.0),
])
def test_varios_casos(notas, pesos, esperado):
assert media_ponderada(notas, pesos) == esperado
Correr: pytest test_media.py -v.