diff --git a/Guia1/src/__pycache__/__init__.cpython-313.pyc b/Guia1/src/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b41715e Binary files /dev/null and b/Guia1/src/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia1/src/__pycache__/main.cpython-313.pyc b/Guia1/src/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..aeeb9fb Binary files /dev/null and b/Guia1/src/__pycache__/main.cpython-313.pyc differ diff --git a/Guia1/src/config/__pycache__/__init__.cpython-313.pyc b/Guia1/src/config/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d0b4b84 Binary files /dev/null and b/Guia1/src/config/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia1/src/config/__pycache__/settings.cpython-313.pyc b/Guia1/src/config/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..f276473 Binary files /dev/null and b/Guia1/src/config/__pycache__/settings.cpython-313.pyc differ diff --git a/Guia1/src/models/__pycache__/__init__.cpython-313.pyc b/Guia1/src/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..678fa76 Binary files /dev/null and b/Guia1/src/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia1/src/models/__pycache__/record.cpython-313.pyc b/Guia1/src/models/__pycache__/record.cpython-313.pyc new file mode 100644 index 0000000..157ad2d Binary files /dev/null and b/Guia1/src/models/__pycache__/record.cpython-313.pyc differ diff --git a/Guia1/src/repositories/__pycache__/__init__.cpython-313.pyc b/Guia1/src/repositories/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..1d22921 Binary files /dev/null and b/Guia1/src/repositories/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia1/src/repositories/__pycache__/abstract_repository.cpython-313.pyc b/Guia1/src/repositories/__pycache__/abstract_repository.cpython-313.pyc new file mode 100644 index 0000000..bc6646b Binary files /dev/null and b/Guia1/src/repositories/__pycache__/abstract_repository.cpython-313.pyc differ diff --git a/Guia1/src/repositories/__pycache__/record_repository.cpython-313.pyc b/Guia1/src/repositories/__pycache__/record_repository.cpython-313.pyc new file mode 100644 index 0000000..83b0a73 Binary files /dev/null and b/Guia1/src/repositories/__pycache__/record_repository.cpython-313.pyc differ diff --git a/Guia1/src/repositories/record_repository.py b/Guia1/src/repositories/record_repository.py index bded279..bafe4ec 100644 --- a/Guia1/src/repositories/record_repository.py +++ b/Guia1/src/repositories/record_repository.py @@ -1,6 +1,7 @@ from src.repositories.abstract_repository import AbstractRepository from src.models.record import Record from src.utils.file_loader import FileLoader +from unidecode import unidecode class RecordRepository(AbstractRepository): @@ -10,15 +11,25 @@ def __init__(self, file_path: str): def load_all(self): data = FileLoader.load_csv(self._file_path) - self._records = [ - Record(int(row["id"]), row["name"], row["address"]) - for row in data - ] + for row in data: + try: + id = int(row['id']) + if row['name'].strip() == '' or row['address'].strip() == '' or id < 0: + raise ValueError() + else: + self._records.append(Record(id, row['name'], row['address'])) + except: + print(f"Registro inválido ignorado {row}") + return self._records def search(self, term: str): - term = term.lower() - return [ - r for r in self._records - if term in r.name.lower() or term in r.address.lower() - ] \ No newline at end of file + terms = unidecode(term).lower().split() + results = [] + + for r in self._records: + nome = unidecode(r.name).lower() + add = unidecode(r.address).lower() + if all(palavra in nome or palavra in add for palavra in terms): + results.append(r) + return results \ No newline at end of file diff --git a/Guia1/src/services/__pycache__/__init__.cpython-313.pyc b/Guia1/src/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..7e17034 Binary files /dev/null and b/Guia1/src/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia1/src/services/__pycache__/record_service.cpython-313.pyc b/Guia1/src/services/__pycache__/record_service.cpython-313.pyc new file mode 100644 index 0000000..aab02d5 Binary files /dev/null and b/Guia1/src/services/__pycache__/record_service.cpython-313.pyc differ diff --git a/Guia1/src/utils/__pycache__/__init__.cpython-313.pyc b/Guia1/src/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..84bd6ee Binary files /dev/null and b/Guia1/src/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia1/src/utils/__pycache__/file_loader.cpython-313.pyc b/Guia1/src/utils/__pycache__/file_loader.cpython-313.pyc new file mode 100644 index 0000000..d06aef0 Binary files /dev/null and b/Guia1/src/utils/__pycache__/file_loader.cpython-313.pyc differ diff --git a/Guia1/tests/__pycache__/__init__.cpython-313.pyc b/Guia1/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..6400053 Binary files /dev/null and b/Guia1/tests/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia1/tests/__pycache__/test_runner.cpython-313-pytest-9.0.3.pyc b/Guia1/tests/__pycache__/test_runner.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000..6a14eba Binary files /dev/null and b/Guia1/tests/__pycache__/test_runner.cpython-313-pytest-9.0.3.pyc differ diff --git a/Guia1/tests/__pycache__/test_runner.cpython-313.pyc b/Guia1/tests/__pycache__/test_runner.cpython-313.pyc new file mode 100644 index 0000000..9d76f5e Binary files /dev/null and b/Guia1/tests/__pycache__/test_runner.cpython-313.pyc differ diff --git a/Guia1/tests/test_runner.py b/Guia1/tests/test_runner.py index f8004a8..005ea34 100644 --- a/Guia1/tests/test_runner.py +++ b/Guia1/tests/test_runner.py @@ -61,7 +61,7 @@ def test_search_multiple_terms(self): for r in results: text = (r.name + " " + r.address).lower() - if "joao" not in text or "rua" not in text or "a" not in text: + if "joão" not in text or "rua" not in text or "a" not in text: print("FALHA: Resultado incorreto na busca") return diff --git a/Guia2/src/__pycache__/__init__.cpython-313.pyc b/Guia2/src/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..13a46a2 Binary files /dev/null and b/Guia2/src/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/__init__.cpython-313.pyc b/Guia2/src/folha_pagamento/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b23a162 Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/desenvolvedor.cpython-313.pyc b/Guia2/src/folha_pagamento/__pycache__/desenvolvedor.cpython-313.pyc new file mode 100644 index 0000000..a39269b Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/desenvolvedor.cpython-313.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/estagiario.cpython-313.pyc b/Guia2/src/folha_pagamento/__pycache__/estagiario.cpython-313.pyc new file mode 100644 index 0000000..456b628 Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/estagiario.cpython-313.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/funcionario.cpython-313.pyc b/Guia2/src/folha_pagamento/__pycache__/funcionario.cpython-313.pyc new file mode 100644 index 0000000..81b44c1 Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/funcionario.cpython-313.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/gerente.cpython-313.pyc b/Guia2/src/folha_pagamento/__pycache__/gerente.cpython-313.pyc new file mode 100644 index 0000000..6c90997 Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/gerente.cpython-313.pyc differ diff --git a/Guia2/src/folha_pagamento/desenvolvedor.py b/Guia2/src/folha_pagamento/desenvolvedor.py index 5c5d3c9..8bb9801 100644 --- a/Guia2/src/folha_pagamento/desenvolvedor.py +++ b/Guia2/src/folha_pagamento/desenvolvedor.py @@ -2,5 +2,37 @@ # Desenvolva a classe Desenvolvedor aqui. -class Desenvolvedor: - pass \ No newline at end of file +class Desenvolvedor(Funcionario): + def __init__(self, nome, matricula, salario_base, linguagem, senioridade): + super().__init__(nome, matricula, salario_base) + if not linguagem: + raise ValueError("Sem valor") + if not senioridade: + raise ValueError("Sem valor") + self.linguagem = linguagem.strip() + self.senioridade = senioridade.strip() + + def calcular_bonus(self): + if self.senioridade == "junior": + return self.salario_base * 0.05 + elif self.senioridade == "pleno": + return self.salario_base * 0.1 + elif self.senioridade == "senior": + return self.salario_base * 0.15 + + def calcular_descontos(self): + return self.salario_base * 0.08 + + def calcular_adicionais(self): + if self.linguagem == "Python": + return 500 + elif self.linguagem == "Java": + return 400 + elif self.linguagem == "JavaScript": + return 350 + else: + return 200 + + + + \ No newline at end of file diff --git a/Guia2/src/folha_pagamento/estagiario.py b/Guia2/src/folha_pagamento/estagiario.py index d50a433..0e543c2 100644 --- a/Guia2/src/folha_pagamento/estagiario.py +++ b/Guia2/src/folha_pagamento/estagiario.py @@ -2,5 +2,26 @@ # Desenvolva a classe Estagiario aqui. -class Estagiario: - pass \ No newline at end of file +class Estagiario(Funcionario): + def __init__(self, nome, matricula, salario_base, curso, carga_horaria): + super().__init__(nome, matricula, salario_base) + if not curso: + raise ValueError("Sem valor") + if not carga_horaria or carga_horaria <= 0: + raise ValueError("Sem valor ou valor incorreto") + self.curso = curso + self.carga_horaria = carga_horaria + + def calcular_bonus(self): + return self.salario_base * 0.03 + + def calcular_descontos(self): + return self.salario_base * 0.02 + + def calcular_adicionais(self): + if self.carga_horaria <= 20: + return 150 + elif self.carga_horaria <= 30: + return 250 + elif self.carga_horaria <= 40: + return 350 \ No newline at end of file diff --git a/Guia2/src/folha_pagamento/gerente.py b/Guia2/src/folha_pagamento/gerente.py index 31819a1..af03325 100644 --- a/Guia2/src/folha_pagamento/gerente.py +++ b/Guia2/src/folha_pagamento/gerente.py @@ -2,5 +2,32 @@ # Desenvolva a classe Gerente aqui. -class Gerente: - pass \ No newline at end of file +class Gerente(Funcionario): + def __init__(self, nome, matricula, salario_base, setor, qtd_equipe): + super().__init__(nome, matricula, salario_base) + if not setor: + raise ValueError("Sem valor") + if not qtd_equipe or qtd_equipe <= 0: + raise ValueError("Sem valor ou valor incorreto") + self.setor = setor + self.qtd_equipe = qtd_equipe + + def calcular_bonus(self): + if self.qtd_equipe <= 5: + return self.salario_base * 0.1 + elif self.qtd_equipe > 5 and self.qtd_equipe <= 10: + return self.salario_base * 0.15 + elif self.qtd_equipe > 10: + return self.salario_base * 0.2 + + def calcular_descontos(self): + return self.salario_base * 0.12 + + def calcular_adicionais(self): + + if self.qtd_equipe > 10: + return 2000 + elif self.qtd_equipe > 5: + return 1000 + else: + return 500 diff --git a/Guia2/tests/__pycache__/__init__.cpython-313.pyc b/Guia2/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..acdf758 Binary files /dev/null and b/Guia2/tests/__pycache__/__init__.cpython-313.pyc differ diff --git a/Guia2/tests/__pycache__/integrity.cpython-313.pyc b/Guia2/tests/__pycache__/integrity.cpython-313.pyc new file mode 100644 index 0000000..e833617 Binary files /dev/null and b/Guia2/tests/__pycache__/integrity.cpython-313.pyc differ diff --git a/Guia2/tests/__pycache__/test_desenvolvedor.cpython-313-pytest-9.0.3.pyc b/Guia2/tests/__pycache__/test_desenvolvedor.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000..3c88159 Binary files /dev/null and b/Guia2/tests/__pycache__/test_desenvolvedor.cpython-313-pytest-9.0.3.pyc differ diff --git a/Guia2/tests/__pycache__/test_estagiario.cpython-313-pytest-9.0.3.pyc b/Guia2/tests/__pycache__/test_estagiario.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000..a3a5d37 Binary files /dev/null and b/Guia2/tests/__pycache__/test_estagiario.cpython-313-pytest-9.0.3.pyc differ diff --git a/Guia2/tests/__pycache__/test_gerente.cpython-313-pytest-9.0.3.pyc b/Guia2/tests/__pycache__/test_gerente.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000..4a6b471 Binary files /dev/null and b/Guia2/tests/__pycache__/test_gerente.cpython-313-pytest-9.0.3.pyc differ diff --git a/Guia2/tests/integrity.py b/Guia2/tests/integrity.py index 6d3ebd9..0e2fbc5 100644 --- a/Guia2/tests/integrity.py +++ b/Guia2/tests/integrity.py @@ -1,3 +1,6 @@ + + + from pathlib import Path import hashlib diff --git a/Guia3/src/alternativa.py b/Guia3/src/alternativa.py index 4dde61f..f2caa32 100644 --- a/Guia3/src/alternativa.py +++ b/Guia3/src/alternativa.py @@ -1,4 +1,10 @@ from typing import List, Tuple, Dict class Alternativa: - pass \ No newline at end of file + def __init__(self, texto, correta, explicacao = None): + self.texto = texto + self.correta = correta + self.explicacao = explicacao + + def get_correta(self): + return self.correta \ No newline at end of file diff --git a/Guia3/src/pergunta.py b/Guia3/src/pergunta.py index 5b3763d..4f00f88 100644 --- a/Guia3/src/pergunta.py +++ b/Guia3/src/pergunta.py @@ -1,4 +1,18 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod -class Pergunta: - pass \ No newline at end of file +class Pergunta(ABC): + def __init__(self, texto, explicacao_geral=None): + self.texto = texto + self.explicacao_geral = explicacao_geral + + @abstractmethod + def validar_resposta(self, resposta): + pass + + def get_explicacao(self): + return self.explicacao_geral + + @abstractmethod + def get_tipo(self): + pass \ No newline at end of file diff --git a/Guia3/src/perguntadiscursiva.py b/Guia3/src/perguntadiscursiva.py index f4c26af..ac82262 100644 --- a/Guia3/src/perguntadiscursiva.py +++ b/Guia3/src/perguntadiscursiva.py @@ -1,4 +1,17 @@ from typing import List, Tuple, Dict +from .pergunta import Pergunta -class PerguntaDiscursiva: - pass \ No newline at end of file +class PerguntaDiscursiva(Pergunta): + def __init__(self, texto, resposta_esperada=None, case_sensitive=True): + super().__init__(texto) + self.resposta_esperada = resposta_esperada + self.case_sensitive = case_sensitive + + def validar_resposta(self, res): + if res == self.resposta_esperada: + return True + else: + return False + + def get_tipo(self): + return "discursiva" \ No newline at end of file diff --git a/Guia3/src/perguntamultiplaescolha.py b/Guia3/src/perguntamultiplaescolha.py index bcbe94d..a37ec0b 100644 --- a/Guia3/src/perguntamultiplaescolha.py +++ b/Guia3/src/perguntamultiplaescolha.py @@ -1,4 +1,23 @@ from typing import List, Tuple, Dict +from .pergunta import Pergunta -class PerguntaMultiplaEscolha: - pass \ No newline at end of file +class PerguntaMultiplaEscolha(Pergunta): + def __init__(self, texto, alternativas, explicacao_geral=None): + super().__init__(texto, explicacao_geral) + self.alternativas = alternativas + + def validar_resposta(self, indice): + resposta = self.alternativas[indice] + return resposta.get_correta() + + def get_alternativa_correta(self): + for res in self.alternativas: + if res.get_correta() == True: + return res + + def get_tipo(self): + return "multipla_escolha" + + def get_explicacao(self): + return self.explicacao_geral + \ No newline at end of file diff --git a/Guia3/src/questionario.py b/Guia3/src/questionario.py index 7525582..b8abb92 100644 --- a/Guia3/src/questionario.py +++ b/Guia3/src/questionario.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from .tentativaquestionario import TentativaQuestionario class Questionario: - pass + def __init__(self, titulo): + self.titulo = titulo + self.perguntas = [] + + def adicionar_pergunta(self, pergunta): + self.perguntas.append(pergunta) + + def criar_attempt(self, usuario): + return TentativaQuestionario(self, usuario) + diff --git a/Guia3/src/resposta.py b/Guia3/src/resposta.py index 846d771..f5add9a 100644 --- a/Guia3/src/resposta.py +++ b/Guia3/src/resposta.py @@ -1,4 +1,15 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod -class Resposta: - pass \ No newline at end of file +class Resposta(ABC): + def __init__(self, pergunta): + self.pergunta = pergunta + + @property + @abstractmethod + def esta_correta(self): + pass + + @abstractmethod + def calcular_pontuacao(self): + pass \ No newline at end of file diff --git a/Guia3/src/respostadiscursiva.py b/Guia3/src/respostadiscursiva.py index 4ea6dbb..2939504 100644 --- a/Guia3/src/respostadiscursiva.py +++ b/Guia3/src/respostadiscursiva.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from .resposta import Resposta -class RespostaDiscursiva: - pass \ No newline at end of file +class RespostaDiscursiva(Resposta): + def __init__(self, pergunta, texto_resposta): + super().__init__(pergunta) + self.texto_resposta = texto_resposta + + @property + def esta_correta(self): + return self.pergunta.validar_resposta(self.texto_resposta) + + def calcular_pontuacao(self): + return 1.0 if self.esta_correta else 0.0 \ No newline at end of file diff --git a/Guia3/src/respostaobjetiva.py b/Guia3/src/respostaobjetiva.py index 72ed2d0..d96d7c1 100644 --- a/Guia3/src/respostaobjetiva.py +++ b/Guia3/src/respostaobjetiva.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from .resposta import Resposta -class RespostaObjetiva: - pass \ No newline at end of file +class RespostaObjetiva(Resposta): + def __init__(self, pergunta, indice_escolhido): + super().__init__(pergunta) + self.indice_escolhido = indice_escolhido + + @property + def esta_correta(self): + return self.pergunta.validar_resposta(self.indice_escolhido) + + def calcular_pontuacao(self): + return 1.0 if self.esta_correta else 0.0 \ No newline at end of file diff --git a/Guia3/src/tentativaquestionario.py b/Guia3/src/tentativaquestionario.py index 9947dd1..bd752f2 100644 --- a/Guia3/src/tentativaquestionario.py +++ b/Guia3/src/tentativaquestionario.py @@ -1,4 +1,33 @@ -from typing import List, Tuple, Dict +from datetime import datetime +from .respostaobjetiva import RespostaObjetiva +from .respostadiscursiva import RespostaDiscursiva +from .perguntamultiplaescolha import PerguntaMultiplaEscolha +from .perguntadiscursiva import PerguntaDiscursiva class TentativaQuestionario: - pass \ No newline at end of file + def __init__(self, questionario, usuario): + self.questionario = questionario + self.usuario = usuario + self.data_inicio = datetime.now() + self.data_fim = None + self.respostas = [] + + def registrar_resposta(self, indice_pergunta, valor): + pergunta = self.questionario.perguntas[indice_pergunta] + if isinstance(pergunta, PerguntaMultiplaEscolha): + resposta = RespostaObjetiva(pergunta, valor) + else: + resposta = RespostaDiscursiva(pergunta, valor) + self.respostas.append(resposta) + + def calcular_pontuacao(self): + return sum(r.calcular_pontuacao() for r in self.respostas) + + def finalizar(self): + self.data_fim = datetime.now() + pontuacao = self.calcular_pontuacao() + feedback = f"Pontuação final: {pontuacao}" + return pontuacao, feedback + + def is_finalizado(self): + return self.data_fim is not None \ No newline at end of file diff --git a/Guia4/README.md b/Guia4/README.md index ca95454..95cb04c 100644 --- a/Guia4/README.md +++ b/Guia4/README.md @@ -93,11 +93,9 @@ classDiagram <> -String api_key -String model - -String base_url + -Groq_obj groq +__init__(api_key: str, model: str = "llama3-70b-8192") - +corrigir_resposta(pergunta: PerguntaDiscursiva, resposta_aluno: str) -> Dict -_fazer_chamada_api(prompt: str) -> str - -_tratar_erro(e: Exception) -> None } class Correcao { diff --git a/Guia4/main.py b/Guia4/main.py index e9e667d..258a8f0 100644 --- a/Guia4/main.py +++ b/Guia4/main.py @@ -1,7 +1,76 @@ -from Guia3.src import * +from dotenv import load_dotenv +load_dotenv() + +from src.alternativa import Alternativa +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha +from src.perguntadiscursiva import PerguntaDiscursiva +from src.questionario import Questionario +from src.correcao import Correcao + def main(): - pass + # 1. Criar o questionário (quiz) + quiz = Questionario("Quiz de Programação") + + # 2. Pergunta de múltipla escolha + p1 = PerguntaMultiplaEscolha( + texto="Qual linguagem é interpretada?", + alternativas=[ + Alternativa("Java", False), + Alternativa("Python", True, explicacao="Python é interpretada por padrão."), + Alternativa("C", False), + ], + explicacao_geral="Python normalmente é interpretada." + ) + quiz.adicionar_pergunta(p1) + + # 3. Pergunta discursiva + p2 = PerguntaDiscursiva( + texto="O que significa CPU?", + resposta_esperada="Central Processing Unit" + ) + quiz.adicionar_pergunta(p2) + + print(f"Quiz criado: '{quiz.titulo}' com {len(quiz.perguntas)} perguntas.\n") + + # 4. Criar tentativa + tentativa = quiz.criar_attempt("valter") + print(f"Tentativa criada para usuário: {tentativa.usuario}") + print(f"Finalizada? {tentativa.is_finalizado()}\n") + + # 5. Registrar respostas + print("--- Registrando respostas ---") + tentativa.registrar_resposta(0, 1) # "Python" -> correta + tentativa.registrar_resposta(1, "Central Processing Unit") # discursiva correta + + print(f"Total de respostas registradas: {len(tentativa.respostas)}\n") + + # 6. Pontuação parcial + print(f"Pontuação atual: {tentativa.calcular_pontuacao()}\n") + + # 7. Finalizar + pontuacao, feedback = tentativa.finalizar() + print("--- Resultado Final ---") + print(f"Pontuação: {pontuacao}") + print(f"Feedback: {feedback}") + print(f"Finalizada? {tentativa.is_finalizado()}\n") + + # 8. Correção discursiva via LLM (Groq) + print("--- Teste de Correção Discursiva via LLM ---") + pergunta_discursiva = PerguntaDiscursiva( + texto="O que é encapsulamento em POO?", + resposta_esperada="É o princípio que esconde os detalhes internos de um objeto, " + "expondo apenas o necessário através de uma interface." + ) + + resposta_aluno = "É quando você protege os atributos da classe e só permite acesso via métodos." + + resultado = Correcao.corrigir_discursiva(pergunta_discursiva, resposta_aluno) + + print(f"Correta: {resultado['correta']}") + print(f"Pontuação: {resultado['pontuacao']}") + print(f"Feedback: {resultado['feedback']}") + print(f"Explicação: {resultado['explicacao']}") if __name__ == "__main__": diff --git a/Guia4/src/alternativa.py b/Guia4/src/alternativa.py index 4dde61f..f2caa32 100644 --- a/Guia4/src/alternativa.py +++ b/Guia4/src/alternativa.py @@ -1,4 +1,10 @@ from typing import List, Tuple, Dict class Alternativa: - pass \ No newline at end of file + def __init__(self, texto, correta, explicacao = None): + self.texto = texto + self.correta = correta + self.explicacao = explicacao + + def get_correta(self): + return self.correta \ No newline at end of file diff --git a/Guia4/src/correcao.py b/Guia4/src/correcao.py index bdf2fa4..c5e998b 100644 --- a/Guia4/src/correcao.py +++ b/Guia4/src/correcao.py @@ -1,4 +1,16 @@ from typing import List, Tuple, Dict +from .llmservice import LLMService + class Correcao: - pass \ No newline at end of file + @staticmethod + def criar_prompt_correcao(pergunta, resposta_aluno): + service = LLMService() + return service._criar_prompt(pergunta, resposta_aluno) + + @staticmethod + def corrigir_discursiva(pergunta, resposta_aluno, service=None): + if service is None: + service = LLMService() + + return service.corrigir_resposta(pergunta, resposta_aluno) \ No newline at end of file diff --git a/Guia4/src/llmservice.py b/Guia4/src/llmservice.py index e6e91b5..168b041 100644 --- a/Guia4/src/llmservice.py +++ b/Guia4/src/llmservice.py @@ -1,4 +1,72 @@ from typing import List, Tuple, Dict +import os +import json +from groq import Groq + + class LLMService: - pass \ No newline at end of file + def __init__(self, api_key=None, model="llama-3.3-70b-versatile"): + self.api_key = api_key or os.environ.get("GROQ_API_KEY") + self.model = model + self.base_url = "https://api.groq.com/openai/v1/chat/completions" + self._client = None + + if self.api_key: + try: + self._client = Groq(api_key=self.api_key) + except Exception as e: + self._tratar_erro(e) + + def corrigir_resposta(self, pergunta, resposta_aluno): + if not self._client: + return { + "correta": False, + "pontuacao": 0.0, + "feedback": "Serviço de correção indisponível (API key não configurada).", + "explicacao": pergunta.get_explicacao() or "" + } + + prompt = self._criar_prompt(pergunta, resposta_aluno) + + try: + resposta_texto = self._fazer_chamada_api(prompt) + resultado = json.loads(resposta_texto) + + return { + "correta": bool(resultado.get("correta", False)), + "pontuacao": float(resultado.get("pontuacao", 0.0)), + "feedback": str(resultado.get("feedback", "")), + "explicacao": str(resultado.get("explicacao", pergunta.get_explicacao() or "")) + } + except Exception as e: + return self._tratar_erro(e) + + def _criar_prompt(self, pergunta, resposta_aluno): + return ( + "Você é um corretor de provas. Avalie a resposta do aluno.\n\n" + f"Pergunta: {pergunta.texto}\n" + f"Resposta esperada: {pergunta.resposta_esperada}\n" + f"Resposta do aluno: {resposta_aluno}\n\n" + "Responda APENAS em JSON, sem texto adicional, no formato:\n" + '{"correta": true ou false, "pontuacao": número de 0.0 a 1.0, ' + '"feedback": "comentário curto sobre a resposta", ' + '"explicacao": "explicação da resposta correta"}' + ) + + def _fazer_chamada_api(self, prompt): + completion = self._client.chat.completions.create( + model=self.model, + messages=[{"role": "user", "content": prompt}], + temperature=0, + response_format={"type": "json_object"}, + ) + return completion.choices[0].message.content + + def _tratar_erro(self, e): + return { + "correta": False, + "pontuacao": 0.0, + "feedback": f"Não foi possível corrigir automaticamente (erro: {type(e).__name__}).", + "explicacao": "" + } \ No newline at end of file diff --git a/Guia4/src/pergunta.py b/Guia4/src/pergunta.py index 5b3763d..4f00f88 100644 --- a/Guia4/src/pergunta.py +++ b/Guia4/src/pergunta.py @@ -1,4 +1,18 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod -class Pergunta: - pass \ No newline at end of file +class Pergunta(ABC): + def __init__(self, texto, explicacao_geral=None): + self.texto = texto + self.explicacao_geral = explicacao_geral + + @abstractmethod + def validar_resposta(self, resposta): + pass + + def get_explicacao(self): + return self.explicacao_geral + + @abstractmethod + def get_tipo(self): + pass \ No newline at end of file diff --git a/Guia4/src/perguntadiscursiva.py b/Guia4/src/perguntadiscursiva.py index f4c26af..ac82262 100644 --- a/Guia4/src/perguntadiscursiva.py +++ b/Guia4/src/perguntadiscursiva.py @@ -1,4 +1,17 @@ from typing import List, Tuple, Dict +from .pergunta import Pergunta -class PerguntaDiscursiva: - pass \ No newline at end of file +class PerguntaDiscursiva(Pergunta): + def __init__(self, texto, resposta_esperada=None, case_sensitive=True): + super().__init__(texto) + self.resposta_esperada = resposta_esperada + self.case_sensitive = case_sensitive + + def validar_resposta(self, res): + if res == self.resposta_esperada: + return True + else: + return False + + def get_tipo(self): + return "discursiva" \ No newline at end of file diff --git a/Guia4/src/perguntamultiplaescolha.py b/Guia4/src/perguntamultiplaescolha.py index bcbe94d..a37ec0b 100644 --- a/Guia4/src/perguntamultiplaescolha.py +++ b/Guia4/src/perguntamultiplaescolha.py @@ -1,4 +1,23 @@ from typing import List, Tuple, Dict +from .pergunta import Pergunta -class PerguntaMultiplaEscolha: - pass \ No newline at end of file +class PerguntaMultiplaEscolha(Pergunta): + def __init__(self, texto, alternativas, explicacao_geral=None): + super().__init__(texto, explicacao_geral) + self.alternativas = alternativas + + def validar_resposta(self, indice): + resposta = self.alternativas[indice] + return resposta.get_correta() + + def get_alternativa_correta(self): + for res in self.alternativas: + if res.get_correta() == True: + return res + + def get_tipo(self): + return "multipla_escolha" + + def get_explicacao(self): + return self.explicacao_geral + \ No newline at end of file diff --git a/Guia4/src/questionario.py b/Guia4/src/questionario.py index 7525582..b8abb92 100644 --- a/Guia4/src/questionario.py +++ b/Guia4/src/questionario.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from .tentativaquestionario import TentativaQuestionario class Questionario: - pass + def __init__(self, titulo): + self.titulo = titulo + self.perguntas = [] + + def adicionar_pergunta(self, pergunta): + self.perguntas.append(pergunta) + + def criar_attempt(self, usuario): + return TentativaQuestionario(self, usuario) + diff --git a/Guia4/src/resposta.py b/Guia4/src/resposta.py index 846d771..f5add9a 100644 --- a/Guia4/src/resposta.py +++ b/Guia4/src/resposta.py @@ -1,4 +1,15 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod -class Resposta: - pass \ No newline at end of file +class Resposta(ABC): + def __init__(self, pergunta): + self.pergunta = pergunta + + @property + @abstractmethod + def esta_correta(self): + pass + + @abstractmethod + def calcular_pontuacao(self): + pass \ No newline at end of file diff --git a/Guia4/src/respostadiscursiva.py b/Guia4/src/respostadiscursiva.py index 4ea6dbb..2939504 100644 --- a/Guia4/src/respostadiscursiva.py +++ b/Guia4/src/respostadiscursiva.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from .resposta import Resposta -class RespostaDiscursiva: - pass \ No newline at end of file +class RespostaDiscursiva(Resposta): + def __init__(self, pergunta, texto_resposta): + super().__init__(pergunta) + self.texto_resposta = texto_resposta + + @property + def esta_correta(self): + return self.pergunta.validar_resposta(self.texto_resposta) + + def calcular_pontuacao(self): + return 1.0 if self.esta_correta else 0.0 \ No newline at end of file diff --git a/Guia4/src/respostaobjetiva.py b/Guia4/src/respostaobjetiva.py index 72ed2d0..d96d7c1 100644 --- a/Guia4/src/respostaobjetiva.py +++ b/Guia4/src/respostaobjetiva.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from .resposta import Resposta -class RespostaObjetiva: - pass \ No newline at end of file +class RespostaObjetiva(Resposta): + def __init__(self, pergunta, indice_escolhido): + super().__init__(pergunta) + self.indice_escolhido = indice_escolhido + + @property + def esta_correta(self): + return self.pergunta.validar_resposta(self.indice_escolhido) + + def calcular_pontuacao(self): + return 1.0 if self.esta_correta else 0.0 \ No newline at end of file diff --git a/Guia4/src/tentativaquestionario.py b/Guia4/src/tentativaquestionario.py index 9947dd1..bd752f2 100644 --- a/Guia4/src/tentativaquestionario.py +++ b/Guia4/src/tentativaquestionario.py @@ -1,4 +1,33 @@ -from typing import List, Tuple, Dict +from datetime import datetime +from .respostaobjetiva import RespostaObjetiva +from .respostadiscursiva import RespostaDiscursiva +from .perguntamultiplaescolha import PerguntaMultiplaEscolha +from .perguntadiscursiva import PerguntaDiscursiva class TentativaQuestionario: - pass \ No newline at end of file + def __init__(self, questionario, usuario): + self.questionario = questionario + self.usuario = usuario + self.data_inicio = datetime.now() + self.data_fim = None + self.respostas = [] + + def registrar_resposta(self, indice_pergunta, valor): + pergunta = self.questionario.perguntas[indice_pergunta] + if isinstance(pergunta, PerguntaMultiplaEscolha): + resposta = RespostaObjetiva(pergunta, valor) + else: + resposta = RespostaDiscursiva(pergunta, valor) + self.respostas.append(resposta) + + def calcular_pontuacao(self): + return sum(r.calcular_pontuacao() for r in self.respostas) + + def finalizar(self): + self.data_fim = datetime.now() + pontuacao = self.calcular_pontuacao() + feedback = f"Pontuação final: {pontuacao}" + return pontuacao, feedback + + def is_finalizado(self): + return self.data_fim is not None \ No newline at end of file diff --git a/Guia5/.gitignore b/Guia5/.gitignore new file mode 100644 index 0000000..07b15a4 --- /dev/null +++ b/Guia5/.gitignore @@ -0,0 +1,8 @@ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +.coverage +htmlcov/ +dist/ +*.egg-info/ diff --git a/Guia5/Guia5_SistemaEscola_README.md b/Guia5/Guia5_SistemaEscola_README.md new file mode 100644 index 0000000..85ea3ae --- /dev/null +++ b/Guia5/Guia5_SistemaEscola_README.md @@ -0,0 +1,330 @@ +# Guia5 — Sistema de Escola (POO) + +## Contexto + +Este projeto implementa um **Sistema de Escola** completo utilizando o paradigma de **Orientação a Objetos** em Python. O sistema gerencia alunos, professores, disciplinas e turmas, aplicando os principais conceitos do curso de POO. + +--- + +## 1. Diagrama UML + +### Diagrama de Classes + +```mermaid +classDiagram + class Pessoa { + <> + -_nome: str + -_cpf: str + +__init__(nome, cpf) + +nome() str + +cpf() str + +apresentar()* str + +__str__() str + } + + class Aluno { + -_matricula: int + -_notas: dict + -_contador_matricula: int$ + +__init__(nome, cpf) + +matricula() int + +notas() dict + +adicionar_nota(disciplina, nota) + +media(disciplina) float + +aprovado(disciplina, minimo) bool + +resetar_contador(valor)$ + +apresentar() str + } + + class Professor { + -_especialidade: str + -_senha_hash: str + -_disciplinas_ministradas: list + +__init__(nome, cpf, especialidade, senha) + +especialidade() str + +disciplinas_ministradas() list + +autenticar(senha) bool + +adicionar_disciplina(nome) + +remover_disciplina(nome) + +apresentar() str + } + + class Disciplina { + -_nome: str + -_carga_horaria: int + -_professor: Professor + +__init__(nome, carga_horaria, professor) + +nome() str + +carga_horaria() int + +professor() Professor + +__str__() str + } + + class Turma { + -_codigo: str + -_serie: str + -_capacidade: int + -_alunos: list + -_disciplinas: list + +__init__(codigo, serie, capacidade) + +vagas_disponiveis() int + +matricular_aluno(aluno) + +remover_aluno(aluno) + +adicionar_disciplina(disciplina) + +gerar_boletim(aluno) dict + +gerar_boletim_turma() dict + +total_alunos() int + +__str__() str + } + + class Validador { + <> + +cpf(cpf)$ tuple + +nome(nome)$ tuple + +senha(senha)$ tuple + +carga_horaria(valor)$ tuple + +capacidade(valor)$ tuple + +nota(valor)$ tuple + +texto_nao_vazio(valor, campo)$ tuple + } + + Pessoa <|-- Aluno : herança + Pessoa <|-- Professor : herança + Disciplina "1" --> "1" Professor : ministrada por + Turma "1" --> "0..*" Aluno : matricula + Turma "1" --> "0..*" Disciplina : possui +``` + +--- + +## 2. Descrição das Classes + +### `Pessoa` (abstrata) +Classe base abstrata (via `ABC`) para qualquer pessoa do sistema escolar. Define atributos comuns `_nome` e `_cpf` com encapsulamento via `@property`, e o método abstrato `apresentar()`, que funciona como um **contrato** obrigando subclasses a defini-lo. + +**Conceitos aplicados:** Classe abstrata, ABC, encapsulamento, `@property`, `@abstractmethod`. + +--- + +### `Aluno` +Herda de `Pessoa`. Representa um estudante matriculado. Gera automaticamente um número de **matrícula sequencial** usando um **atributo de classe** (`_contador_matricula`) e um **método de classe** (`resetar_contador`). Armazena notas por disciplina e calcula médias e situação de aprovação. + +**Conceitos aplicados:** Herança, atributo de classe, método de classe (`@classmethod`), composição de dicionário, validação com `@property.setter`. + +--- + +### `Professor` +Herda de `Pessoa`. Representa um docente. Além dos dados básicos e da lista de disciplinas ministradas, armazena um **hash SHA-256 da senha** para autenticação segura. O método `autenticar(senha)` permite verificar o acesso sem expor a senha em texto puro. O lançamento de notas no sistema exige autenticação prévia de um professor. + +**Conceitos aplicados:** Herança, encapsulamento, `@property.setter` com validação, lista mutável encapsulada, hash de senha. + +--- + +### `Disciplina` +Representa uma matéria escolar com nome, carga horária e professor responsável. Na criação, já notifica o professor via `adicionar_disciplina()`. Ao trocar de professor (via setter), remove o vínculo do anterior e cria o vínculo com o novo — demonstrando **composição bidirecional**. + +**Conceitos aplicados:** Composição, `@property.setter` com efeito colateral, referência entre objetos. + +--- + +### `Turma` +Agrega alunos e disciplinas. Controla a capacidade máxima de alunos e garante que não haja duplicatas. O método `gerar_boletim(aluno)` retorna o boletim completo de **um aluno específico**: para cada disciplina da turma, lista as notas lançadas, a média calculada e a situação (Aprovado/Reprovado/Sem notas). O método `gerar_boletim_turma()` retorna o boletim de **todos os alunos matriculados** de uma vez, reutilizando `gerar_boletim` internamente. + +**Conceitos aplicados:** Composição (lista de `Aluno` e `Disciplina`), encapsulamento de listas (retorno de cópias), reuso de método dentro da própria classe. + +--- + +### `Validador` *(novo)* +Classe utilitária com **métodos estáticos** que centralizam toda a lógica de validação de entradas do sistema. Cada método retorna uma tupla `(bool, resultado_ou_mensagem)`, onde `True` indica sucesso e `False` indica falha acompanhada de uma mensagem descritiva. O `main.py` usa esses métodos em loops que repetem a solicitação ao usuário até receber um valor válido. + +| Método | O que valida | +|--------|-------------| +| `cpf(cpf)` | Exatamente 11 dígitos numéricos; aceita pontos e traços; rejeita sequências de dígitos iguais | +| `nome(nome)` | Apenas letras e espaços; obrigatório ter ao menos nome + sobrenome | +| `senha(senha)` | Mínimo de 4 caracteres | +| `carga_horaria(valor)` | Inteiro positivo entre 1 e 400 | +| `capacidade(valor)` | Inteiro positivo entre 1 e 100 | +| `nota(valor)` | Número entre 0 e 10; aceita vírgula como separador decimal | +| `texto_nao_vazio(valor, campo)` | Campo genérico não vazio | + +**Conceitos aplicados:** Classe utilitária, métodos estáticos (`@staticmethod`), separação de responsabilidades, padrão de retorno consistente. + +--- + +## 3. Estrutura do Projeto + +``` +sistema-escola/ +├── src/ ← classes de domínio +│ ├── __init__.py ← exporta Pessoa, Aluno, Professor, Disciplina, Turma +│ ├── pessoa.py +│ ├── aluno.py +│ ├── professor.py +│ ├── disciplina.py +│ └── turma.py +├── utils/ ← ferramentas de suporte +│ ├── __init__.py ← exporta Validador + todas as funções de leitura +│ ├── validador.py ← classe Validador (CPF, nome, nota…) +│ └── leitores.py ← ler_*, escolher_da_lista, separador, autenticar_professor +├── tests/ +│ ├── __init__.py +│ ├── test_aluno.py +│ ├── test_professor.py +│ ├── test_disciplina.py +│ ├── test_turma.py +│ ├── test_sistema_escola.py +│ └── test_validador.py +├── main.py +├── requirements.txt +├── .gitignore +└── Guia5_SistemaEscola_README.md +``` + +### Separação de responsabilidades + +| Pasta | O que contém | Critério | +|-------|-------------|----------| +| `src/` | Classes de domínio (`Aluno`, `Professor`, `Turma`…) | Representam as entidades do problema | +| `utils/` | `Validador` + funções `ler_*` / `escolher_da_lista` | Ferramentas de suporte sem lógica de negócio | +| `tests/` | Suíte completa de testes pytest | Um arquivo por classe testada | + +--- + +## 4. Preparando o Ambiente + +### 4.1 Criar o ambiente virtual + +Na pasta do projeto `...\Guia5>`, execute: + +```bash +python -m venv .venv +``` + +### 4.2 Ativar o ambiente virtual + +#### Windows + +```bash +.\.venv\Scripts\activate +``` + +#### Linux / macOS + +```bash +source .venv/bin/activate +``` + +### 4.3 Instalar dependências + +```bash +pip install -r requirements.txt +``` + +### 4.4 Executar os testes + +```bash +pytest -v +``` + +ou + +```bash +python -m pytest -v +``` + +Para ver a **cobertura de testes**: + +```bash +pytest -v --cov=src --cov-report=term-missing +``` + +### 4.5 Executar o sistema + +```bash +python main.py +``` + +--- + +## 5. Funcionalidades do Sistema (main.py) + +Ao rodar `python main.py`, o terminal exibe um menu interativo com as seguintes opções: + +| Opção | Funcionalidade | +|-------|----------------| +| `[1]` | **Cadastrar Professor** — informa nome completo, CPF, especialidade e **senha** (mín. 4 caracteres) | +| `[2]` | **Cadastrar Disciplina** — escolhe o professor em uma lista e define carga horária | +| `[3]` | **Criar Turma** — define código (ex: `9A`), série e capacidade máxima; permite vincular disciplinas escolhendo-as em lista | +| `[4]` | **Matricular Aluno** — escolhe a turma em lista, depois cadastra o aluno (nome completo + CPF) | +| `[5]` | **Lançar Nota** *(requer acesso de professor)* — o professor se autentica com sua senha; só pode lançar notas nas disciplinas que ministra | +| `[6]` | **Boletim de um Aluno** — escolhe **turma** → escolhe **aluno da turma**; exibe notas, média e situação por disciplina | +| `[7]` | **Boletim da Turma Inteira** — escolhe a turma em lista; exibe o boletim de todos os alunos matriculados | +| `[8]` | **Listar Turmas** — exibe todas as turmas com suas disciplinas e alunos | +| `[0]` | **Sair** | + +> **Nenhuma opção do menu exige buscar algo digitando um nome ou código.** Toda seleção (professor, disciplina, turma, aluno) é feita por **lista numerada**. + +> **Validação em loop:** todos os campos de entrada passam pela classe `Validador`. Caso o valor seja inválido, o sistema exibe uma mensagem de erro clara e solicita nova digitação até receber um valor correto. + +> **Padronização de texto:** todo campo de texto (exceto senha) passa por `.strip().upper()` antes de ser processado. + +--- + +## 5.1 Fluxo de Autenticação para Lançar Nota + +``` +[5] Lançar Nota + └─ Lista de professores cadastrados + └─ Escolhe o professor + └─ Digita a senha + ├─ Senha correta → escolhe aluno → escolhe disciplina (só as do professor) → digita nota + └─ Senha errada → "Acesso negado." → volta ao menu +``` + +O professor só visualiza e pode lançar notas nas disciplinas que ele próprio ministra. + +--- + +## 5.2 Fluxo do Boletim de um Aluno + +``` +[6] Boletim de um Aluno + └─ Lista todas as turmas cadastradas + └─ Escolhe a turma + └─ Lista os alunos da turma selecionada + └─ Escolhe o aluno → exibe boletim +``` + +A etapa intermediária de seleção de turma garante que o usuário veja apenas os alunos da turma de interesse. + +--- + +## 6. Exemplo de uso típico + +1. Cadastre um professor: `Rodrigo Alves | Matemática | senha: mat123` +2. Crie a disciplina `Matemática (80h)` vinculada a ele +3. Crie a turma `9A | 9º Ano | capacidade: 30` +4. Matricule alunos na turma +5. Lance notas: autentique-se como professor → escolha aluno → escolha disciplina → informe nota +6. Consulte o boletim: escolha a turma → escolha o aluno → veja o resultado + +--- + +## 7. Conceitos de POO Aplicados + +| Conceito | Onde aparece | +|----------|-------------| +| **Classes e objetos** | Todas as classes | +| **Encapsulamento** | `_atributos` com `@property` e validação nos setters | +| **Herança** | `Aluno` e `Professor` herdam de `Pessoa` | +| **Polimorfismo** | `apresentar()` redefinido em cada subclasse; `__str__` delega a `apresentar()` | +| **Classe abstrata / ABC** | `Pessoa` com `@abstractmethod apresentar()` | +| **Atributo de classe** | `Aluno._contador_matricula` | +| **Método de classe** | `Aluno.resetar_contador()` com `@classmethod` | +| **Método estático** | `Validador.*` com `@staticmethod` | +| **Classe utilitária** | `Validador` em `utils/` — centraliza toda a lógica de validação | +| **Separação de camadas** | `src/` (domínio) vs `utils/` (suporte) — cada pasta com responsabilidade única | +| **Composição** | `Turma` contém listas de `Aluno` e `Disciplina`; `Disciplina` referencia `Professor` | +| **Autenticação com hash** | `Professor._senha_hash` usando SHA-256 | +| **Validação e exceções** | `ValueError`, `OverflowError` em cenários de negócio; `Validador` em entradas do usuário | diff --git a/Guia5/dados.py b/Guia5/dados.py new file mode 100644 index 0000000..ea4bec9 --- /dev/null +++ b/Guia5/dados.py @@ -0,0 +1,120 @@ +from src import * +from utils import * + +def popular_banco( + turmas: list, + professores: list, + disciplinas: list, + alunos: list, +) -> None: + """Popula as listas do sistema com dados de exemplo para demonstração. + + Deve ser chamada no início de menu_principal(), passando as próprias + listas já criadas lá. Exemplo de uso: + + def menu_principal() -> None: + turmas, professores, disciplinas, alunos = [], [], [], [] + popular_banco(turmas, professores, disciplinas, alunos) + ... + """ + + # ── Professores (nome com sobrenome, CPF 11 dígitos sem repetição, senha ≥ 4) ── + prof_mat = Professor("RODRIGO ALVES", "12345678901", "MATEMATICA", "mat123") + prof_por = Professor("ANA LIMA", "23456789012", "PORTUGUES", "por123") + prof_cis = Professor("BEATRIZ COSTA", "34567890123", "CIENCIAS", "cis123") + prof_his = Professor("CARLOS MENDES", "45678901234", "HISTORIA", "his123") + professores.extend([prof_mat, prof_por, prof_cis, prof_his]) + + # ── Disciplinas (nome não vazio, carga horária 1–400) ──────────────────────── + disc_mat = Disciplina("MATEMATICA", 80, prof_mat) + disc_por = Disciplina("PORTUGUES", 80, prof_por) + disc_cis = Disciplina("CIENCIAS", 60, prof_cis) + disc_his = Disciplina("HISTORIA", 60, prof_his) + disc_geo = Disciplina("GEOGRAFIA", 40, prof_his) + disciplinas.extend([disc_mat, disc_por, disc_cis, disc_his, disc_geo]) + + # ── Turmas (código e série não vazios, capacidade 1–100) ───────────────────── + turma_9a = Turma("9A", "9 ANO", 30) + turma_9b = Turma("9B", "9 ANO", 30) + turma_8a = Turma("8A", "8 ANO", 30) + + for disc in [disc_mat, disc_por, disc_cis, disc_his, disc_geo]: + turma_9a.adicionar_disciplina(disc) + turma_9b.adicionar_disciplina(disc) + + for disc in [disc_mat, disc_por, disc_cis]: + turma_8a.adicionar_disciplina(disc) + + turmas.extend([turma_9a, turma_9b, turma_8a]) + + # ── Alunos turma 9A ────────────────────────────────────────────────────────── + dados_9a = [ + ("ALICE SOUZA", "56789012345"), + ("BRUNO SILVA", "67890123456"), + ("CAMILA SANTOS", "78901234567"), + ("DANIEL LIMA", "89012345678"), + ("EDUARDA COSTA", "90123456789"), + ] + alunos_9a = [] + for nome, cpf in dados_9a: + a = Aluno(nome, cpf) + turma_9a.matricular_aluno(a) + alunos_9a.append(a) + alunos.append(a) + + # ── Alunos turma 9B ────────────────────────────────────────────────────────── + dados_9b = [ + ("FELIPE ROCHA", "10234567891"), + ("GABRIELA NUNES", "20345678912"), + ("HENRIQUE DIAS", "30456789123"), + ] + alunos_9b = [] + for nome, cpf in dados_9b: + a = Aluno(nome, cpf) + turma_9b.matricular_aluno(a) + alunos_9b.append(a) + alunos.append(a) + + # ── Alunos turma 8A ────────────────────────────────────────────────────────── + dados_8a = [ + ("IGOR FERREIRA", "40567891234"), + ("JULIA MORAES", "50678912345"), + ("LUCAS MARTINS", "60789123456"), + ] + alunos_8a = [] + for nome, cpf in dados_8a: + a = Aluno(nome, cpf) + turma_8a.matricular_aluno(a) + alunos_8a.append(a) + alunos.append(a) + + # ── Notas turma 9A ─────────────────────────────────────────────────────────── + notas_9a = { + "ALICE SOUZA": {"MATEMATICA": [9.0, 8.5], "PORTUGUES": [7.0, 8.0], "CIENCIAS": [9.5, 10.0], "HISTORIA": [6.0, 7.5], "GEOGRAFIA": [8.0]}, + "BRUNO SILVA": {"MATEMATICA": [4.0, 5.5], "PORTUGUES": [6.0, 6.5], "CIENCIAS": [3.0, 4.0], "HISTORIA": [5.0, 5.5], "GEOGRAFIA": [5.0]}, + "CAMILA SANTOS": {"MATEMATICA": [7.0, 7.5], "PORTUGUES": [9.0, 9.5], "CIENCIAS": [7.0, 8.0], "HISTORIA": [8.0, 8.5], "GEOGRAFIA": [9.0]}, + "DANIEL LIMA": {"MATEMATICA": [6.0, 6.5], "PORTUGUES": [5.0, 5.5], "CIENCIAS": [6.5, 7.0], "HISTORIA": [4.0, 3.5], "GEOGRAFIA": [6.0]}, + "EDUARDA COSTA": {"MATEMATICA": [8.0, 9.0], "PORTUGUES": [8.5, 7.5], "CIENCIAS": [9.0, 9.5], "HISTORIA": [7.0, 8.0], "GEOGRAFIA": [8.5]}, + } + for aluno in alunos_9a: + for disc, notas in notas_9a[aluno.nome].items(): + for n in notas: + aluno.adicionar_nota(disc, n) + + # ── Notas turma 9B (parcialmente lançadas) ─────────────────────────────────── + notas_9b = { + "FELIPE ROCHA": {"MATEMATICA": [7.0], "PORTUGUES": [6.5]}, + "GABRIELA NUNES": {"MATEMATICA": [9.5], "CIENCIAS": [8.0]}, + "HENRIQUE DIAS": {"HISTORIA": [5.0], "GEOGRAFIA": [4.5]}, + } + for aluno in alunos_9b: + for disc, notas in notas_9b[aluno.nome].items(): + for n in notas: + aluno.adicionar_nota(disc, n) + + # ── Notas turma 8A (sem notas — turma recém-criada para demonstrar estado inicial) + # alunos_8a propositalmente sem notas lançadas ────────────────────────── + + print("✔ Banco de dados de exemplo carregado com sucesso!") + print(f" {len(professores)} professores | {len(disciplinas)} disciplinas | " + f"{len(turmas)} turmas | {len(alunos)} alunos") diff --git a/Guia5/main.py b/Guia5/main.py new file mode 100644 index 0000000..07576f2 --- /dev/null +++ b/Guia5/main.py @@ -0,0 +1,225 @@ +from src import * +from utils import * +from dados import popular_banco + + +def menu_principal() -> None: + turmas: list[Turma] = [] + professores: list[Professor] = [] + disciplinas: list[Disciplina] = [] + alunos: list[Aluno] = [] + + popular_banco(turmas, professores, disciplinas, alunos) + + opcoes = { + "1": "Cadastrar Professor", + "2": "Cadastrar Disciplina", + "3": "Criar Turma", + "4": "Matricular Aluno", + "5": "Lançar Nota [requer acesso de professor]", + "6": "Exibir Boletim de um Aluno", + "7": "Exibir Boletim da Turma Inteira", + "8": "Listar Turmas", + "0": "Sair", + } + + while True: + separador("SISTEMA DE ESCOLA — MENU PRINCIPAL") + for k, v in opcoes.items(): + print(f" [{k}] {v}") + escolha = input("\nEscolha uma opção: ").strip() + + # ── 1. Cadastrar Professor ──────────────────────────────────────────── + if escolha == "1": + separador("CADASTRAR PROFESSOR") + nome = ler_nome("Nome completo: ") + cpf = ler_cpf("CPF (somente números): ") + esp = ler_texto_validado("Especialidade: ", "Especialidade") + senha = ler_senha("Senha (mín. 4 caracteres): ") + try: + prof = Professor(nome, cpf, esp, senha) + professores.append(prof) + print(f"\n✔ {prof} cadastrado com sucesso!") + except ValueError as e: + print(f"\n✖ Erro: {e}") + + # ── 2. Cadastrar Disciplina ─────────────────────────────────────────── + elif escolha == "2": + separador("CADASTRAR DISCIPLINA") + prof_sel = escolher_da_lista(professores, "professor") + if prof_sel is None: + continue + nome_disc = ler_texto_validado("Nome da disciplina: ", "Nome da disciplina") + ch = ler_carga_horaria() + try: + disc = Disciplina(nome_disc, ch, prof_sel) + disciplinas.append(disc) + print(f"\n✔ {disc} cadastrada com sucesso!") + except ValueError as e: + print(f"\n✖ Erro: {e}") + + # ── 3. Criar Turma ─────────────────────────────────────────────────── + elif escolha == "3": + separador("CRIAR TURMA") + codigo = ler_texto_validado("Código da turma (ex: 9A): ", "Código") + serie = ler_texto_validado("Série (ex: 9º ANO): ", "Série") + cap = ler_capacidade() + try: + t = Turma(codigo, serie, cap) + turmas.append(t) + print(f"\n✔ {t} criada com sucesso!") + except ValueError as e: + print(f"\n✖ Erro: {e}") + continue + + if disciplinas: + print("\nDeseja adicionar disciplinas a esta turma?") + while True: + disponiveis = [d for d in disciplinas if d not in t.disciplinas] + if not disponiveis: + print("Todas as disciplinas já foram adicionadas.") + break + disc_sel = escolher_da_lista(disponiveis, "disciplina") + if disc_sel is None: + break + t.adicionar_disciplina(disc_sel) + print(f"✔ '{disc_sel.nome}' adicionada à turma.") + if input("Adicionar outra disciplina? (s/n): ").strip().upper() != "S": + break + + # ── 4. Matricular Aluno ────────────────────────────────────────────── + elif escolha == "4": + separador("MATRICULAR ALUNO") + turma_sel = escolher_da_lista(turmas, "turma") + if turma_sel is None: + continue + nome = ler_nome("Nome completo do aluno: ") + cpf = ler_cpf("CPF do aluno (somente números): ") + try: + aluno = Aluno(nome, cpf) + turma_sel.matricular_aluno(aluno) + alunos.append(aluno) + print(f"\n✔ {aluno} matriculado em '{turma_sel.codigo}'!") + except (ValueError, OverflowError) as e: + print(f"\n✖ Erro: {e}") + + # ── 5. Lançar Nota [requer acesso de professor] ───────────────────── + elif escolha == "5": + separador("LANÇAR NOTA") + + if not professores: + print("✖ Nenhum professor cadastrado. Cadastre um professor primeiro.") + continue + + prof_autenticado = autenticar_professor(professores) + if prof_autenticado is None: + continue + + alunos_disponiveis = [ + a for a in alunos + if any( + a in t.alunos and any(d.professor == prof_autenticado for d in t.disciplinas) + for t in turmas + ) + ] + if not alunos_disponiveis: + print("✖ Nenhum aluno encontrado nas turmas com disciplinas deste professor.") + continue + + aluno_sel = escolher_da_lista(alunos_disponiveis, "aluno") + if aluno_sel is None: + continue + + turma_do_aluno = next((t for t in turmas if aluno_sel in t.alunos), None) + if turma_do_aluno is None: + print("✖ Turma do aluno não encontrada.") + continue + + discs_professor = [d for d in turma_do_aluno.disciplinas if d.professor == prof_autenticado] + if not discs_professor: + print("✖ Este professor não ministra disciplinas na turma deste aluno.") + continue + + disc_sel = escolher_da_lista(discs_professor, "disciplina") + if disc_sel is None: + continue + + nota = ler_nota() + try: + aluno_sel.adicionar_nota(disc_sel.nome, nota) + print(f"\n✔ Nota {nota} registrada para '{aluno_sel.nome}' em '{disc_sel.nome}'.") + except ValueError as e: + print(f"\n✖ Erro: {e}") + + # ── 6. Boletim de um Aluno ─────────────────────────────────────────── + elif escolha == "6": + separador("BOLETIM DO ALUNO") + if not turmas: + print("✖ Nenhuma turma cadastrada.") + continue + print("Selecione a turma:") + turma_sel = escolher_da_lista(turmas, "turma") + if turma_sel is None: + continue + if not turma_sel.alunos: + print(f"✖ A turma '{turma_sel.codigo}' não possui alunos matriculados.") + continue + print(f"\nAlunos da turma {turma_sel.codigo} — {turma_sel.serie}:") + aluno_sel = escolher_da_lista(turma_sel.alunos, "aluno") + if aluno_sel is None: + continue + try: + boletim = turma_sel.gerar_boletim(aluno_sel) + print(f"\nBoletim de {aluno_sel.nome} — Turma {turma_sel.codigo}") + print(f"{'DISCIPLINA':<20} {'NOTAS':<20} {'MÉDIA':>8} {'SITUAÇÃO':>12}") + print("-" * 62) + if not boletim: + print("Nenhuma disciplina cadastrada nesta turma.") + for disc_nome, dados in boletim.items(): + notas_str = ", ".join(str(n) for n in dados["notas"]) or "—" + media_str = f"{dados['media']:.2f}" if dados["media"] is not None else "—" + print(f"{disc_nome:<20} {notas_str:<20} {media_str:>8} {dados['situacao']:>12}") + except ValueError as e: + print(f"\n✖ Erro: {e}") + + # ── 7. Boletim da Turma Inteira ────────────────────────────────────── + elif escolha == "7": + separador("BOLETIM DA TURMA INTEIRA") + turma_sel = escolher_da_lista(turmas, "turma") + if turma_sel is None: + continue + boletim_turma = turma_sel.gerar_boletim_turma() + if not boletim_turma: + print("✖ Nenhum aluno matriculado nesta turma.") + continue + for nome_aluno, boletim in boletim_turma.items(): + print(f"\n• {nome_aluno}") + print(f" {'DISCIPLINA':<20} {'NOTAS':<20} {'MÉDIA':>8} {'SITUAÇÃO':>12}") + print(" " + "-" * 60) + if not boletim: + print(" Nenhuma disciplina cadastrada nesta turma.") + for disc_nome, dados in boletim.items(): + notas_str = ", ".join(str(n) for n in dados["notas"]) or "—" + media_str = f"{dados['media']:.2f}" if dados["media"] is not None else "—" + print(f" {disc_nome:<20} {notas_str:<20} {media_str:>8} {dados['situacao']:>12}") + + # ── 8. Listar Turmas ───────────────────────────────────────────────── + elif escolha == "8": + separador("TURMAS CADASTRADAS") + if not turmas: + print("Nenhuma turma cadastrada.") + for t in turmas: + print(f"\n{t}") + print(" Disciplinas:", ", ".join(d.nome for d in t.disciplinas) or "—") + print(" Alunos:", ", ".join(a.nome for a in t.alunos) or "—") + + # ── 0. Sair ────────────────────────────────────────────────────────── + elif escolha == "0": + print("\nAté logo! 👋\n") + break + else: + print("\n✖ Opção inválida. Tente novamente.") + + +if __name__ == "__main__": + menu_principal() diff --git a/Guia5/src/__init__.py b/Guia5/src/__init__.py new file mode 100644 index 0000000..1f53bbc --- /dev/null +++ b/Guia5/src/__init__.py @@ -0,0 +1,7 @@ +from .pessoa import Pessoa +from .aluno import Aluno +from .professor import Professor +from .disciplina import Disciplina +from .turma import Turma + +__all__ = ["Pessoa", "Aluno", "Professor", "Disciplina", "Turma"] diff --git a/Guia5/src/aluno.py b/Guia5/src/aluno.py new file mode 100644 index 0000000..cb48abf --- /dev/null +++ b/Guia5/src/aluno.py @@ -0,0 +1,43 @@ +from .pessoa import Pessoa + + +class Aluno(Pessoa): + _contador_matricula: int = 1000 # atributo de classe + + def __init__(self, nome: str, cpf: str): + super().__init__(nome, cpf) + Aluno._contador_matricula += 1 + self._matricula: int = Aluno._contador_matricula + self._notas: dict[str, list[float]] = {} # disciplina_nome → [notas] + + @property + def matricula(self) -> int: + return self._matricula + + @property + def notas(self) -> dict[str, list[float]]: + return {k: list(v) for k, v in self._notas.items()} + + def adicionar_nota(self, disciplina: str, nota: float) -> None: + if not (0.0 <= nota <= 10.0): + raise ValueError(f"Nota deve estar entre 0 e 10. Recebido: {nota}") + self._notas.setdefault(disciplina, []).append(nota) + + def media(self, disciplina: str) -> float: + notas = self._notas.get(disciplina) + if not notas: + raise ValueError(f"Nenhuma nota registrada para '{disciplina}'.") + return sum(notas) / len(notas) + + def aprovado(self, disciplina: str, minimo: float = 6.0) -> bool: + return self.media(disciplina) >= minimo + + @classmethod + def resetar_contador(cls, valor: int = 1000) -> None: + cls._contador_matricula = valor + + def apresentar(self) -> str: + return f"Aluno: {self._nome} | Matrícula: {self._matricula}" + + def __repr__(self) -> str: + return f"Aluno(nome={self._nome!r}, matricula={self._matricula})" diff --git a/Guia5/src/disciplina.py b/Guia5/src/disciplina.py new file mode 100644 index 0000000..fa7035e --- /dev/null +++ b/Guia5/src/disciplina.py @@ -0,0 +1,48 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .professor import Professor + + +class Disciplina: + + def __init__(self, nome: str, carga_horaria: int, professor: "Professor"): + if not nome or not nome.strip(): + raise ValueError("Nome da disciplina não pode ser vazio.") + if carga_horaria <= 0: + raise ValueError("Carga horária deve ser positiva.") + self._nome: str = nome.strip() + self._carga_horaria: int = carga_horaria + self._professor: Professor = professor + professor.adicionar_disciplina(self._nome) + + @property + def nome(self) -> str: + return self._nome + + @property + def carga_horaria(self) -> int: + return self._carga_horaria + + @carga_horaria.setter + def carga_horaria(self, valor: int) -> None: + if valor <= 0: + raise ValueError("Carga horária deve ser positiva.") + self._carga_horaria = valor + + @property + def professor(self) -> "Professor": + return self._professor + + @professor.setter + def professor(self, novo: "Professor") -> None: + self._professor.remover_disciplina(self._nome) + self._professor = novo + novo.adicionar_disciplina(self._nome) + + def __str__(self) -> str: + return f"Disciplina: {self._nome} ({self._carga_horaria}h) | Prof. {self._professor.nome}" + + def __repr__(self) -> str: + return f"Disciplina(nome={self._nome!r}, carga_horaria={self._carga_horaria})" diff --git a/Guia5/src/pessoa.py b/Guia5/src/pessoa.py new file mode 100644 index 0000000..396ecd5 --- /dev/null +++ b/Guia5/src/pessoa.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod + + +class Pessoa(ABC): + + def __init__(self, nome: str, cpf: str): + if not nome or not nome.strip(): + raise ValueError("Nome não pode ser vazio.") + if not cpf or not cpf.strip(): + raise ValueError("CPF não pode ser vazio.") + self._nome = nome.strip() + self._cpf = cpf.strip() + + @property + def nome(self) -> str: + return self._nome + + @nome.setter + def nome(self, valor: str): + if not valor or not valor.strip(): + raise ValueError("Nome não pode ser vazio.") + self._nome = valor.strip() + + @property + def cpf(self) -> str: + return self._cpf + + @abstractmethod + def apresentar(self) -> str: + """Retorna uma apresentação textual da pessoa.""" + + def __str__(self) -> str: + return self.apresentar() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(nome={self._nome!r}, cpf={self._cpf!r})" diff --git a/Guia5/src/professor.py b/Guia5/src/professor.py new file mode 100644 index 0000000..b67e428 --- /dev/null +++ b/Guia5/src/professor.py @@ -0,0 +1,50 @@ +import hashlib +from .pessoa import Pessoa + + +class Professor(Pessoa): + + def __init__(self, nome: str, cpf: str, especialidade: str, senha: str): + super().__init__(nome, cpf) + if not especialidade or not especialidade.strip(): + raise ValueError("Especialidade não pode ser vazia.") + if not senha or len(senha) < 4: + raise ValueError("Senha deve ter ao menos 4 caracteres.") + self._especialidade: str = especialidade.strip() + self._disciplinas_ministradas: list[str] = [] + self._senha_hash: str = hashlib.sha256(senha.encode()).hexdigest() + + @property + def especialidade(self) -> str: + return self._especialidade + + @especialidade.setter + def especialidade(self, valor: str) -> None: + if not valor or not valor.strip(): + raise ValueError("Especialidade não pode ser vazia.") + self._especialidade = valor.strip() + + @property + def disciplinas_ministradas(self) -> list[str]: + return list(self._disciplinas_ministradas) + + def autenticar(self, senha: str) -> bool: + return hashlib.sha256(senha.encode()).hexdigest() == self._senha_hash + + def adicionar_disciplina(self, nome_disciplina: str) -> None: + nome = nome_disciplina.strip() + if nome not in self._disciplinas_ministradas: + self._disciplinas_ministradas.append(nome) + + def remover_disciplina(self, nome_disciplina: str) -> None: + nome = nome_disciplina.strip() + if nome in self._disciplinas_ministradas: + self._disciplinas_ministradas.remove(nome) + else: + raise ValueError(f"Disciplina '{nome}' não encontrada para este professor.") + + def apresentar(self) -> str: + return f"Prof. {self._nome} | Especialidade: {self._especialidade}" + + def __repr__(self) -> str: + return f"Professor(nome={self._nome!r}, especialidade={self._especialidade!r})" diff --git a/Guia5/src/turma.py b/Guia5/src/turma.py new file mode 100644 index 0000000..2953f0e --- /dev/null +++ b/Guia5/src/turma.py @@ -0,0 +1,97 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .aluno import Aluno + from .disciplina import Disciplina + + +class Turma: + + def __init__(self, codigo: str, serie: str, capacidade: int = 40): + if not codigo or not codigo.strip(): + raise ValueError("Código da turma não pode ser vazio.") + if capacidade <= 0: + raise ValueError("Capacidade deve ser positiva.") + self._codigo: str = codigo.strip() + self._serie: str = serie.strip() + self._capacidade: int = capacidade + self._alunos: list[Aluno] = [] + self._disciplinas: list[Disciplina] = [] + + @property + def codigo(self) -> str: + return self._codigo + + @property + def serie(self) -> str: + return self._serie + + @property + def capacidade(self) -> int: + return self._capacidade + + @property + def alunos(self) -> list[Aluno]: + return list(self._alunos) + + @property + def disciplinas(self) -> list[Disciplina]: + return list(self._disciplinas) + + @property + def vagas_disponiveis(self) -> int: + return self._capacidade - len(self._alunos) + + def matricular_aluno(self, aluno: "Aluno") -> None: + if self.vagas_disponiveis == 0: + raise OverflowError(f"Turma '{self._codigo}' está cheia.") + if aluno in self._alunos: + raise ValueError(f"Aluno '{aluno.nome}' já está matriculado nesta turma.") + self._alunos.append(aluno) + + def remover_aluno(self, aluno: "Aluno") -> None: + if aluno not in self._alunos: + raise ValueError(f"Aluno '{aluno.nome}' não encontrado na turma.") + self._alunos.remove(aluno) + + def adicionar_disciplina(self, disciplina: "Disciplina") -> None: + if disciplina in self._disciplinas: + raise ValueError(f"Disciplina '{disciplina.nome}' já está na turma.") + self._disciplinas.append(disciplina) + + def gerar_boletim(self, aluno: "Aluno") -> dict[str, dict]: + if aluno not in self._alunos: + raise ValueError(f"Aluno '{aluno.nome}' não está matriculado nesta turma.") + + boletim = {} + for disciplina in self._disciplinas: + nome_disc = disciplina.nome + notas = aluno.notas.get(nome_disc, []) + if notas: + media = round(sum(notas) / len(notas), 2) + situacao = "Aprovado" if media >= 6.0 else "Reprovado" + else: + media = None + situacao = "Sem notas" + boletim[nome_disc] = { + "notas": notas, + "media": media, + "situacao": situacao, + } + return boletim + + def gerar_boletim_turma(self) -> dict[str, dict[str, dict]]: + return {aluno.nome: self.gerar_boletim(aluno) for aluno in self._alunos} + + def total_alunos(self) -> int: + return len(self._alunos) + + def __str__(self) -> str: + return ( + f"Turma {self._codigo} | {self._serie} | " + f"{len(self._alunos)}/{self._capacidade} alunos" + ) + + def __repr__(self) -> str: + return f"Turma(codigo={self._codigo!r}, serie={self._serie!r}, capacidade={self._capacidade})" diff --git a/Guia5/tests/__init__.py b/Guia5/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Guia5/tests/test_aluno.py b/Guia5/tests/test_aluno.py new file mode 100644 index 0000000..29ed260 --- /dev/null +++ b/Guia5/tests/test_aluno.py @@ -0,0 +1,82 @@ +import pytest +from src.aluno import Aluno + + +@pytest.fixture(autouse=True) +def resetar_matricula(): + Aluno.resetar_contador(1000) + yield + + +class TestAlunoCriacao: + def test_criar_aluno_valido(self): + a = Aluno("Maria Silva", "111.222.333-44") + assert a.nome == "Maria Silva" + assert a.cpf == "111.222.333-44" + assert a.matricula == 1001 + + def test_matriculas_sao_incrementadas(self): + a1 = Aluno("Ana", "000.000.000-01") + a2 = Aluno("Bia", "000.000.000-02") + assert a2.matricula == a1.matricula + 1 + + def test_nome_vazio_levanta_erro(self): + with pytest.raises(ValueError): + Aluno("", "111.222.333-44") + + def test_cpf_vazio_levanta_erro(self): + with pytest.raises(ValueError): + Aluno("Maria", "") + + +class TestAlunoNotas: + def test_adicionar_nota_valida(self): + a = Aluno("Carlos", "222.333.444-55") + a.adicionar_nota("Matemática", 8.5) + assert 8.5 in a.notas["Matemática"] + + def test_nota_fora_do_intervalo(self): + a = Aluno("Carlos", "222.333.444-55") + with pytest.raises(ValueError): + a.adicionar_nota("Matemática", 11.0) + + def test_nota_negativa(self): + a = Aluno("Carlos", "222.333.444-55") + with pytest.raises(ValueError): + a.adicionar_nota("Matemática", -1.0) + + def test_media_calculada_corretamente(self): + a = Aluno("Diana", "333.444.555-66") + a.adicionar_nota("Física", 7.0) + a.adicionar_nota("Física", 9.0) + assert a.media("Física") == pytest.approx(8.0) + + def test_media_sem_notas_levanta_erro(self): + a = Aluno("Eva", "444.555.666-77") + with pytest.raises(ValueError): + a.media("Química") + + def test_aprovado_acima_do_minimo(self): + a = Aluno("Fábio", "555.666.777-88") + a.adicionar_nota("História", 7.0) + assert a.aprovado("História") is True + + def test_reprovado_abaixo_do_minimo(self): + a = Aluno("Gabriela", "666.777.888-99") + a.adicionar_nota("História", 4.0) + assert a.aprovado("História") is False + + def test_notas_retorna_copia(self): + a = Aluno("Henrique", "777.888.999-00") + a.adicionar_nota("Arte", 9.0) + copia = a.notas + copia["Arte"].append(1.0) + assert len(a.notas["Arte"]) == 1 + + +class TestAlunoStr: + def test_str_contem_nome_e_matricula(self): + a = Aluno("Igor", "888.999.000-11") + s = str(a) + assert "Igor" in s + assert str(a.matricula) in s diff --git a/Guia5/tests/test_disciplina.py b/Guia5/tests/test_disciplina.py new file mode 100644 index 0000000..bfdb824 --- /dev/null +++ b/Guia5/tests/test_disciplina.py @@ -0,0 +1,56 @@ +import pytest +from src.professor import Professor +from src.disciplina import Disciplina + + +@pytest.fixture +def professor(): + return Professor("Rodrigo Alves", "12345678900", "Física", "senha123") + + +class TestDisciplinaCriacao: + def test_criar_disciplina_valida(self, professor): + d = Disciplina("Física Clássica", 80, professor) + assert d.nome == "Física Clássica" + assert d.carga_horaria == 80 + assert d.professor is professor + + def test_nome_vazio_levanta_erro(self, professor): + with pytest.raises(ValueError): + Disciplina("", 80, professor) + + def test_carga_horaria_invalida(self, professor): + with pytest.raises(ValueError): + Disciplina("Física", 0, professor) + + def test_professor_recebe_disciplina_automaticamente(self, professor): + Disciplina("Física Moderna", 60, professor) + assert "Física Moderna" in professor.disciplinas_ministradas + + +class TestDisciplinaSetters: + def test_setter_carga_horaria(self, professor): + d = Disciplina("Física", 80, professor) + d.carga_horaria = 120 + assert d.carga_horaria == 120 + + def test_setter_carga_horaria_invalida(self, professor): + d = Disciplina("Física", 80, professor) + with pytest.raises(ValueError): + d.carga_horaria = -10 + + def test_trocar_professor(self, professor): + novo_prof = Professor("Beatriz Costa", "98765432100", "Física", "senha123") + d = Disciplina("Física", 80, professor) + d.professor = novo_prof + assert d.professor is novo_prof + assert "Física" not in professor.disciplinas_ministradas + assert "Física" in novo_prof.disciplinas_ministradas + + +class TestDisciplinaStr: + def test_str_contem_nome_e_professor(self, professor): + d = Disciplina("Física", 80, professor) + s = str(d) + assert "Física" in s + assert professor.nome in s diff --git a/Guia5/tests/test_professor.py b/Guia5/tests/test_professor.py new file mode 100644 index 0000000..3a1d43a --- /dev/null +++ b/Guia5/tests/test_professor.py @@ -0,0 +1,86 @@ +import pytest +from src.professor import Professor + + +SENHA_PADRAO = "senha123" + + +def make_professor(nome="João Souza", cpf="99988877766", esp="Matemática", senha=SENHA_PADRAO): + return Professor(nome, cpf, esp, senha) + + +class TestProfessorCriacao: + def test_criar_professor_valido(self): + p = make_professor() + assert p.nome == "João Souza" + assert p.especialidade == "Matemática" + + def test_especialidade_vazia_levanta_erro(self): + with pytest.raises(ValueError): + Professor("João Souza", "99988877766", "", SENHA_PADRAO) + + def test_senha_curta_levanta_erro(self): + with pytest.raises(ValueError): + Professor("João Souza", "99988877766", "Matemática", "abc") + + def test_setter_especialidade(self): + p = make_professor() + p.especialidade = "Física" + assert p.especialidade == "Física" + + def test_setter_especialidade_vazia_levanta_erro(self): + p = make_professor() + with pytest.raises(ValueError): + p.especialidade = " " + + +class TestProfessorAutenticacao: + def test_autenticar_senha_correta(self): + p = make_professor() + assert p.autenticar(SENHA_PADRAO) is True + + def test_autenticar_senha_errada(self): + p = make_professor() + assert p.autenticar("senhaerrada") is False + + def test_autenticar_senha_vazia(self): + p = make_professor() + assert p.autenticar("") is False + + +class TestProfessorDisciplinas: + def test_adicionar_disciplina(self): + p = make_professor(nome="Ana Lima", cpf="11100022233", esp="Química") + p.adicionar_disciplina("Química Orgânica") + assert "Química Orgânica" in p.disciplinas_ministradas + + def test_disciplina_duplicada_ignorada(self): + p = make_professor(nome="Ana Lima", cpf="11100022233", esp="Química") + p.adicionar_disciplina("Química Orgânica") + p.adicionar_disciplina("Química Orgânica") + assert p.disciplinas_ministradas.count("Química Orgânica") == 1 + + def test_remover_disciplina(self): + p = make_professor(nome="Ana Lima", cpf="11100022233", esp="Química") + p.adicionar_disciplina("Química Orgânica") + p.remover_disciplina("Química Orgânica") + assert "Química Orgânica" not in p.disciplinas_ministradas + + def test_remover_disciplina_inexistente_levanta_erro(self): + p = make_professor(nome="Ana Lima", cpf="11100022233", esp="Química") + with pytest.raises(ValueError): + p.remover_disciplina("Biologia") + + def test_disciplinas_retorna_copia(self): + p = make_professor(nome="Ana Lima", cpf="11100022233", esp="Química") + lista = p.disciplinas_ministradas + lista.append("Invasora") + assert "Invasora" not in p.disciplinas_ministradas + + +class TestProfessorStr: + def test_str_contem_nome_e_especialidade(self): + p = make_professor(nome="Carlos Matos", cpf="33344455566", esp="Biologia") + s = str(p) + assert "Carlos Matos" in s + assert "Biologia" in s diff --git a/Guia5/tests/test_sistema_escola.py b/Guia5/tests/test_sistema_escola.py new file mode 100644 index 0000000..bd72d6c --- /dev/null +++ b/Guia5/tests/test_sistema_escola.py @@ -0,0 +1,70 @@ +""" +Testes de integração do Sistema de Escola. +Simulam um cenário realista com turma, disciplina, professor e alunos. +""" +import pytest +from src.aluno import Aluno +from src.professor import Professor +from src.disciplina import Disciplina +from src.turma import Turma + + +SENHA = "senha123" + + +@pytest.fixture(autouse=True) +def resetar_matricula(): + Aluno.resetar_contador(1000) + yield + + +def test_fluxo_completo_turma(): + """Cria turma, matricula alunos, lança notas e gera boletim.""" + prof = Professor("Marcos Vinicius", "01002003040", "Ciências", SENHA) + mat = Disciplina("Ciências", 60, prof) + turma = Turma("6B", "6º Ano", capacidade=5) + turma.adicionar_disciplina(mat) + + nomes = ["Alice Silva", "Bruno Costa", "Camila Dias"] + alunos = [Aluno(n, f"0000000000{i+1}") for i, n in enumerate(nomes)] + for a in alunos: + turma.matricular_aluno(a) + + alunos[0].adicionar_nota("Ciências", 9.0) + alunos[1].adicionar_nota("Ciências", 5.5) + alunos[2].adicionar_nota("Ciências", 7.0) + alunos[2].adicionar_nota("Ciências", 9.0) + + boletim_turma = turma.gerar_boletim_turma() + + assert boletim_turma["Alice Silva"]["Ciências"]["situacao"] == "Aprovado" + assert boletim_turma["Bruno Costa"]["Ciências"]["situacao"] == "Reprovado" + assert boletim_turma["Camila Dias"]["Ciências"]["media"] == pytest.approx(8.0) + assert boletim_turma["Camila Dias"]["Ciências"]["situacao"] == "Aprovado" + + +def test_professor_ministra_varias_disciplinas(): + prof = Professor("Juliana Ramos", "05006007080", "Português", SENHA) + Disciplina("Português", 80, prof) + Disciplina("Literatura", 40, prof) + assert "Português" in prof.disciplinas_ministradas + assert "Literatura" in prof.disciplinas_ministradas + + +def test_professor_autenticacao(): + prof = Professor("Rodrigo Alves", "12345678901", "Matemática", SENHA) + assert prof.autenticar(SENHA) is True + assert prof.autenticar("senhaerrada") is False + + +def test_pessoa_nao_pode_ser_instanciada_diretamente(): + from src.pessoa import Pessoa + with pytest.raises(TypeError): + Pessoa("Teste Silva", "00000000000") # type: ignore + + +def test_aluno_apresentar(): + a = Aluno("Eduardo Lima", "12312312312") + apresentacao = a.apresentar() + assert "Eduardo Lima" in apresentacao + assert str(a.matricula) in apresentacao diff --git a/Guia5/tests/test_turma.py b/Guia5/tests/test_turma.py new file mode 100644 index 0000000..d62b31e --- /dev/null +++ b/Guia5/tests/test_turma.py @@ -0,0 +1,147 @@ +import pytest +from src.aluno import Aluno +from src.professor import Professor +from src.disciplina import Disciplina +from src.turma import Turma + + +@pytest.fixture(autouse=True) +def resetar_matricula(): + Aluno.resetar_contador(1000) + yield + + +@pytest.fixture +def professor(): + return Professor("Letícia Nunes", "00011122233", "Matemática", "senha123") + + +@pytest.fixture +def disciplina(professor): + return Disciplina("Matemática", 80, professor) + + +@pytest.fixture +def turma(): + return Turma("9A", "9º Ano", capacidade=3) + + +@pytest.fixture +def alunos(): + return [Aluno(f"Aluno {i}", f"00{i}.000.000-00") for i in range(1, 4)] + + +class TestTurmaCriacao: + def test_criar_turma_valida(self, turma): + assert turma.codigo == "9A" + assert turma.serie == "9º Ano" + assert turma.capacidade == 3 + + def test_codigo_vazio_levanta_erro(self): + with pytest.raises(ValueError): + Turma("", "9º Ano") + + def test_capacidade_invalida(self): + with pytest.raises(ValueError): + Turma("9A", "9º Ano", capacidade=0) + + +class TestTurmaMatricula: + def test_matricular_aluno(self, turma, alunos): + turma.matricular_aluno(alunos[0]) + assert alunos[0] in turma.alunos + + def test_turma_cheia_levanta_erro(self, turma, alunos): + for a in alunos: + turma.matricular_aluno(a) + extra = Aluno("Extra", "999.999.999-99") + with pytest.raises(OverflowError): + turma.matricular_aluno(extra) + + def test_aluno_duplicado_levanta_erro(self, turma, alunos): + turma.matricular_aluno(alunos[0]) + with pytest.raises(ValueError): + turma.matricular_aluno(alunos[0]) + + def test_remover_aluno(self, turma, alunos): + turma.matricular_aluno(alunos[0]) + turma.remover_aluno(alunos[0]) + assert alunos[0] not in turma.alunos + + def test_remover_aluno_ausente_levanta_erro(self, turma, alunos): + with pytest.raises(ValueError): + turma.remover_aluno(alunos[0]) + + def test_vagas_disponiveis(self, turma, alunos): + turma.matricular_aluno(alunos[0]) + assert turma.vagas_disponiveis == 2 + + def test_total_alunos(self, turma, alunos): + turma.matricular_aluno(alunos[0]) + turma.matricular_aluno(alunos[1]) + assert turma.total_alunos() == 2 + + +class TestTurmaDisciplinas: + def test_adicionar_disciplina(self, turma, disciplina): + turma.adicionar_disciplina(disciplina) + assert disciplina in turma.disciplinas + + def test_disciplina_duplicada_levanta_erro(self, turma, disciplina): + turma.adicionar_disciplina(disciplina) + with pytest.raises(ValueError): + turma.adicionar_disciplina(disciplina) + + +class TestTurmaBoletim: + def test_boletim_com_notas(self, turma, alunos, disciplina): + turma.matricular_aluno(alunos[0]) + turma.adicionar_disciplina(disciplina) + alunos[0].adicionar_nota("Matemática", 8.0) + alunos[0].adicionar_nota("Matemática", 6.0) + boletim = turma.gerar_boletim(alunos[0]) + assert boletim["Matemática"]["media"] == pytest.approx(7.0) + assert boletim["Matemática"]["situacao"] == "Aprovado" + assert boletim["Matemática"]["notas"] == [8.0, 6.0] + + def test_boletim_sem_notas(self, turma, alunos, disciplina): + turma.matricular_aluno(alunos[0]) + turma.adicionar_disciplina(disciplina) + boletim = turma.gerar_boletim(alunos[0]) + assert boletim["Matemática"]["situacao"] == "Sem notas" + assert boletim["Matemática"]["media"] is None + + def test_boletim_reprovado(self, turma, alunos, disciplina): + turma.matricular_aluno(alunos[0]) + turma.adicionar_disciplina(disciplina) + alunos[0].adicionar_nota("Matemática", 3.0) + boletim = turma.gerar_boletim(alunos[0]) + assert boletim["Matemática"]["situacao"] == "Reprovado" + + def test_boletim_aluno_nao_matriculado_levanta_erro(self, turma, alunos): + with pytest.raises(ValueError): + turma.gerar_boletim(alunos[0]) + + def test_boletim_turma_inteira(self, turma, alunos, disciplina): + turma.adicionar_disciplina(disciplina) + turma.matricular_aluno(alunos[0]) + turma.matricular_aluno(alunos[1]) + alunos[0].adicionar_nota("Matemática", 9.0) + alunos[1].adicionar_nota("Matemática", 4.0) + + boletim_turma = turma.gerar_boletim_turma() + + assert alunos[0].nome in boletim_turma + assert alunos[1].nome in boletim_turma + assert boletim_turma[alunos[0].nome]["Matemática"]["situacao"] == "Aprovado" + assert boletim_turma[alunos[1].nome]["Matemática"]["situacao"] == "Reprovado" + + def test_boletim_turma_vazia(self, turma): + assert turma.gerar_boletim_turma() == {} + + +class TestTurmaStr: + def test_str_contem_codigo_e_serie(self, turma): + s = str(turma) + assert "9A" in s + assert "9º Ano" in s diff --git a/Guia5/tests/test_validador.py b/Guia5/tests/test_validador.py new file mode 100644 index 0000000..490a560 --- /dev/null +++ b/Guia5/tests/test_validador.py @@ -0,0 +1,132 @@ +import pytest +from utils import Validador + + +class TestValidadorCpf: + def test_cpf_valido_somente_digitos(self): + ok, resultado = Validador.cpf("12345678901") + assert ok is True + assert resultado == "12345678901" + + def test_cpf_valido_com_formatacao(self): + ok, resultado = Validador.cpf("123.456.789-01") + assert ok is True + assert resultado == "12345678901" + + def test_cpf_com_menos_de_11_digitos(self): + ok, msg = Validador.cpf("1234567890") + assert ok is False + assert "11 dígitos" in msg + + def test_cpf_com_mais_de_11_digitos(self): + ok, msg = Validador.cpf("123456789012") + assert ok is False + assert "11 dígitos" in msg + + def test_cpf_com_letras(self): + ok, msg = Validador.cpf("1234567890A") + assert ok is False + assert "apenas números" in msg + + def test_cpf_todos_iguais(self): + ok, msg = Validador.cpf("11111111111") + assert ok is False + assert "inválido" in msg.lower() + + def test_cpf_vazio(self): + ok, msg = Validador.cpf("") + assert ok is False + + +class TestValidadorNome: + def test_nome_valido(self): + ok, resultado = Validador.nome("João Silva") + assert ok is True + + def test_nome_sem_sobrenome(self): + ok, msg = Validador.nome("João") + assert ok is False + assert "sobrenome" in msg.lower() or "duas palavras" in msg.lower() + + def test_nome_com_numeros(self): + ok, msg = Validador.nome("João123 Silva") + assert ok is False + assert "letras" in msg.lower() + + def test_nome_vazio(self): + ok, msg = Validador.nome("") + assert ok is False + + def test_nome_com_acentos(self): + ok, resultado = Validador.nome("Lívia Gonçalves") + assert ok is True + + +class TestValidadorSenha: + def test_senha_valida(self): + ok, resultado = Validador.senha("abcd") + assert ok is True + + def test_senha_curta(self): + ok, msg = Validador.senha("abc") + assert ok is False + assert "4 caracteres" in msg + + def test_senha_vazia(self): + ok, msg = Validador.senha("") + assert ok is False + + +class TestValidadorCargaHoraria: + def test_carga_valida(self): + ok, resultado = Validador.carga_horaria("80") + assert ok is True + assert resultado == 80 + + def test_carga_zero(self): + ok, msg = Validador.carga_horaria("0") + assert ok is False + + def test_carga_negativa(self): + ok, msg = Validador.carga_horaria("-10") + assert ok is False + + def test_carga_texto(self): + ok, msg = Validador.carga_horaria("oitenta") + assert ok is False + + def test_carga_acima_limite(self): + ok, msg = Validador.carga_horaria("401") + assert ok is False + + +class TestValidadorNota: + def test_nota_valida(self): + ok, resultado = Validador.nota("7.5") + assert ok is True + assert resultado == pytest.approx(7.5) + + def test_nota_virgula(self): + ok, resultado = Validador.nota("8,5") + assert ok is True + assert resultado == pytest.approx(8.5) + + def test_nota_zero(self): + ok, resultado = Validador.nota("0") + assert ok is True + + def test_nota_dez(self): + ok, resultado = Validador.nota("10") + assert ok is True + + def test_nota_acima_de_dez(self): + ok, msg = Validador.nota("10.1") + assert ok is False + + def test_nota_negativa(self): + ok, msg = Validador.nota("-1") + assert ok is False + + def test_nota_texto(self): + ok, msg = Validador.nota("nota") + assert ok is False diff --git a/Guia5/utils/__init__.py b/Guia5/utils/__init__.py new file mode 100644 index 0000000..99fae0f --- /dev/null +++ b/Guia5/utils/__init__.py @@ -0,0 +1,31 @@ +from .validador import Validador +from .leitores import ( + separador, + escolher_da_lista, + ler_texto, + ler_texto_simples, + ler_cpf, + ler_nome, + ler_senha, + ler_carga_horaria, + ler_capacidade, + ler_nota, + ler_texto_validado, + autenticar_professor, +) + +__all__ = [ + "Validador", + "separador", + "escolher_da_lista", + "ler_texto", + "ler_texto_simples", + "ler_cpf", + "ler_nome", + "ler_senha", + "ler_carga_horaria", + "ler_capacidade", + "ler_nota", + "ler_texto_validado", + "autenticar_professor", +] diff --git a/Guia5/utils/leitores.py b/Guia5/utils/leitores.py new file mode 100644 index 0000000..e31d84e --- /dev/null +++ b/Guia5/utils/leitores.py @@ -0,0 +1,106 @@ +from .validador import Validador + +def separador(titulo: str = "") -> None: + print("\n" + "=" * 55) + if titulo: + print(f" {titulo}") + print("=" * 55) + + +def escolher_da_lista(lista: list, rotulo_item: str = "item") -> "object | None": + if not lista: + print(f"✖ Nenhum(a) {rotulo_item} cadastrado(a).") + return None + for i, item in enumerate(lista): + print(f" {i + 1}. {item}") + try: + idx = int(input(f"\nEscolha o número do(a) {rotulo_item}: ")) - 1 + if 0 <= idx < len(lista): + return lista[idx] + print("✖ Número fora da lista.") + return None + except ValueError: + print("✖ Entrada inválida. Digite um número.") + return None + +def ler_texto(rotulo: str) -> str: + return input(rotulo).strip().upper() + + +def ler_texto_simples(rotulo: str) -> str: + return input(rotulo).strip() + + +def ler_cpf(rotulo: str = "CPF: ") -> str: + while True: + valor = input(rotulo).strip() + ok, resultado = Validador.cpf(valor) + if ok: + return resultado + print(f" ✖ {resultado}") + + +def ler_nome(rotulo: str = "Nome: ") -> str: + while True: + valor = ler_texto(rotulo) + ok, resultado = Validador.nome(valor) + if ok: + return resultado + print(f" ✖ {resultado}") + + +def ler_senha(rotulo: str = "Senha: ") -> str: + while True: + valor = ler_texto_simples(rotulo) + ok, resultado = Validador.senha(valor) + if ok: + return resultado + print(f" ✖ {resultado}") + + +def ler_carga_horaria(rotulo: str = "Carga horária (horas): ") -> int: + while True: + valor = input(rotulo).strip() + ok, resultado = Validador.carga_horaria(valor) + if ok: + return resultado + print(f" ✖ {resultado}") + + +def ler_capacidade(rotulo: str = "Capacidade máxima de alunos: ") -> int: + while True: + valor = input(rotulo).strip() + ok, resultado = Validador.capacidade(valor) + if ok: + return resultado + print(f" ✖ {resultado}") + + +def ler_nota(rotulo: str = "Nota (0–10): ") -> float: + while True: + valor = input(rotulo).strip() + ok, resultado = Validador.nota(valor) + if ok: + return resultado + print(f" ✖ {resultado}") + + +def ler_texto_validado(rotulo: str, campo: str) -> str: + while True: + valor = ler_texto(rotulo) + ok, resultado = Validador.texto_nao_vazio(valor, campo) + if ok: + return resultado + print(f" ✖ {resultado}") + +def autenticar_professor(professores: list) -> "object | None": + print("\nIdentifique-se como professor para lançar notas.") + prof_sel = escolher_da_lista(professores, "professor") + if prof_sel is None: + return None + senha = ler_texto_simples("Senha do professor: ") + if prof_sel.autenticar(senha): + print(f"✔ Acesso concedido. Bem-vindo, {prof_sel.nome}!") + return prof_sel + print("✖ Senha incorreta. Acesso negado.") + return None diff --git a/Guia5/utils/validador.py b/Guia5/utils/validador.py new file mode 100644 index 0000000..ef854c6 --- /dev/null +++ b/Guia5/utils/validador.py @@ -0,0 +1,75 @@ +import re + + +class Validador: + """Classe utilitária com métodos estáticos de validação de dados de entrada.""" + + @staticmethod + def cpf(cpf: str) -> tuple[bool, str]: + cpf_limpo = re.sub(r"[\.\-]", "", cpf).strip() + if not cpf_limpo.isdigit(): + return False, "CPF deve conter apenas números (e opcionalmente pontos/traços)." + if len(cpf_limpo) != 11: + return False, f"CPF deve ter exatamente 11 dígitos. Informado: {len(cpf_limpo)} dígito(s)." + if len(set(cpf_limpo)) == 1: + return False, "CPF inválido (todos os dígitos iguais)." + return True, cpf_limpo + + @staticmethod + def nome(nome: str) -> tuple[bool, str]: + nome_limpo = nome.strip() + if not nome_limpo: + return False, "Nome não pode ser vazio." + if not re.match(r"^[A-ZÀ-Ÿa-zà-ÿ\s]+$", nome_limpo): + return False, "Nome deve conter apenas letras e espaços (sem números ou símbolos)." + partes = nome_limpo.split() + if len(partes) < 2: + return False, "Informe nome e sobrenome (ao menos duas palavras)." + return True, nome_limpo + + @staticmethod + def senha(senha: str) -> tuple[bool, str]: + if len(senha) < 4: + return False, "A senha deve ter ao menos 4 caracteres." + return True, senha + + @staticmethod + def carga_horaria(valor: str) -> tuple[bool, str]: + try: + ch = int(valor) + except ValueError: + return False, "Carga horária deve ser um número inteiro." + if ch <= 0: + return False, "Carga horária deve ser positiva." + if ch > 400: + return False, "Carga horária não pode ultrapassar 400 horas." + return True, ch + + @staticmethod + def capacidade(valor: str) -> tuple[bool, str]: + try: + cap = int(valor) + except ValueError: + return False, "Capacidade deve ser um número inteiro." + if cap <= 0: + return False, "Capacidade deve ser positiva." + if cap > 100: + return False, "Capacidade máxima permitida é 100 alunos." + return True, cap + + @staticmethod + def nota(valor: str) -> tuple[bool, str]: + try: + nota = float(valor.replace(",", ".")) + except ValueError: + return False, "Nota deve ser um número (ex: 7.5)." + if not (0.0 <= nota <= 10.0): + return False, f"Nota deve estar entre 0 e 10. Informado: {nota}." + return True, nota + + @staticmethod + def texto_nao_vazio(valor: str, campo: str = "Campo") -> tuple[bool, str]: + limpo = valor.strip() + if not limpo: + return False, f"{campo} não pode ser vazio." + return True, limpo