Partilhar: WhatsApp
aulify
UC UC00607 · T. Desenv. Software

Ficha 02 · Módulos, JSON, APIs e testes

Organizar código, manipular dados, testar com pytest
Versão · Aluno
Tempo · 60 minutos
Cotação · 100 pontos
Aluno(a)
Turma
Data
Objectivos da ficha

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.