verifica 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
verifica/__init__.py ADDED
File without changes
verifica/__main__.py ADDED
@@ -0,0 +1,85 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ from colorama import init
4
+ import logging
5
+ import sys
6
+
7
+ from .config import settings
8
+ from .check import Checker
9
+ from .fetcher import Fetcher
10
+ from .builder import Builder
11
+
12
+ logging.basicConfig(
13
+ level=logging.ERROR,
14
+ format='%(levelname)s - %(message)s',
15
+ handlers=[logging.FileHandler(f'{settings.config_dir}/app.log', mode='w', encoding='utf-8'), logging.StreamHandler(stream=sys.stdout)]
16
+ )
17
+ init(autoreset=True)
18
+
19
+ def main():
20
+ parser = argparse.ArgumentParser(description="Uma ferramenta simples para correção de atividades em python via CLI.", add_help=False)
21
+ positional = parser.add_argument_group("argumentos posicionais")
22
+ options = parser.add_argument_group("opções")
23
+
24
+ positional.add_argument("atividade", nargs="?", help="A URL do github ou caminho local do arquivo usado para a correção da atividade.")
25
+ options.add_argument("-f", "--files", default=Path.cwd(), help="Diretório com a(s) atividade(s) a ser(em) corrigida(s).")
26
+ options.add_argument("-h", "--help", "--ajuda", action="help", help="Mostra essa mensagem de ajuda.")
27
+ options.add_argument("-c", "--config", action="store_true", help="Mostra o caminho do arquivo de configuração.")
28
+ options.add_argument("-d", "--debug", action="store_true", help="Ativa o modo debug, mostrando mais informações durante a execução.")
29
+ options.add_argument("-l", "--local", action="store_true", help="Caminho local para a pasta com o arquivo de respostas, caso não queira baixar do github.")
30
+ options.add_argument("--version", action="store_true", help="Mostra a versão do programa.")
31
+ options.add_argument("-b", "--builder", action="store_true", help="TUI para criar arquivos de correção de atividades.")
32
+
33
+ args = parser.parse_args()
34
+
35
+ if args.debug:
36
+ logging.getLogger().setLevel(logging.DEBUG)
37
+ logging.info("Modo debug ativado. Mostrando informações detalhadas durante a execução.")
38
+
39
+ if args.config:
40
+ print(settings.config_path)
41
+ return
42
+
43
+ if args.version:
44
+ print(settings.get_version())
45
+ return
46
+
47
+ if args.builder:
48
+ builder = Builder()
49
+ builder.start()
50
+ return
51
+
52
+ if not args.atividade:
53
+ parser.error("o seguinte argumento é obrigatório: atividade")
54
+
55
+ if not args.local:
56
+ answers = Fetcher(args.atividade)
57
+ try:
58
+ print("Baixando arquivo de correção...")
59
+ logging.debug(f"CAMINHO DO ARQUIVO BAIXADO: {answers.fetch()}")
60
+ except Exception as e:
61
+ logging.error(f"Não foi possível localizar o arquivo de correção em '{args.atividade}'")
62
+ sys.exit(1)
63
+ else:
64
+ answers = Fetcher(args.atividade, local=True)
65
+ try:
66
+ print("Buscando arquivo de correção local...")
67
+ logging.debug(f"CAMINHO DO ARQUIVO LOCAL: {answers.get_file()}")
68
+ except Exception as e:
69
+ logging.error(f"Não foi possível localizar o arquivo de correção em '{args.atividade}'")
70
+ sys.exit(1)
71
+
72
+ decoded_answers = answers.get_decoded_json()
73
+ answers.cleanup()
74
+
75
+ checker = Checker(args.files, decoded_answers)
76
+
77
+ checker.setup_roadmap()
78
+
79
+ results = checker.run_roadmap()
80
+ checker.show_results(results)
81
+
82
+ sys.exit(0)
83
+
84
+ if __name__ == "__main__":
85
+ main()
verifica/builder.py ADDED
@@ -0,0 +1,431 @@
1
+ from pathlib import Path
2
+ from colorama import Style, Fore
3
+ from functools import wraps
4
+ import subprocess
5
+ import json
6
+ import os
7
+
8
+ from .tui import TUI
9
+ from .config import settings
10
+
11
+ class Controller:
12
+ @staticmethod
13
+ def clear_command():
14
+ if os.name == 'nt':
15
+ subprocess.run(['cmd', '/c', 'cls'])
16
+ else:
17
+ subprocess.run(['clear'])
18
+
19
+
20
+ def clear_terminal(func):
21
+ @wraps(func)
22
+ def wrapper(*args, **kwargs):
23
+ Controller.clear_command()
24
+ return func(*args, **kwargs)
25
+ return wrapper
26
+
27
+
28
+ def sequential_question(func):
29
+ @wraps(func)
30
+ def wrapper(*args, **kwargs):
31
+ answers = []
32
+ while True:
33
+ result = func(*args, **kwargs)
34
+ if result.strip() != "":
35
+ answers.append(result)
36
+ else:
37
+ return answers
38
+ return wrapper
39
+
40
+
41
+ def required_question(func):
42
+ @wraps(func)
43
+ def wrapper(*args, **kwargs):
44
+ while True:
45
+ result = func(*args, **kwargs)
46
+ if not isinstance(result, str) or result.strip() == "":
47
+ print(f"{Style.BRIGHT}{Fore.RED}Este campo é obrigatório. Por favor, insira um valor válido.{Style.RESET_ALL}")
48
+ else:
49
+ return result
50
+ return wrapper
51
+
52
+
53
+ def required_text(text) -> str:
54
+ return f"{Fore.RED}{text}{Style.RESET_ALL}"
55
+
56
+
57
+ def optional_text(text) -> str:
58
+ return f"{Fore.YELLOW}{text}{Style.RESET_ALL}"
59
+
60
+
61
+ def sequence_text(text) -> str:
62
+ return f"{Fore.GREEN}{text}{Style.RESET_ALL}"
63
+
64
+
65
+ class Selector:
66
+ def __init__(self, name: str, action, selected: bool = False):
67
+ self.name = name
68
+ self.action = action
69
+ self.selected = selected
70
+
71
+ class FileBuilder(Controller):
72
+ def __init__(self, file: str):
73
+ self.file = file
74
+ self.selectors = [
75
+ Selector("Classe", self.create_classes),
76
+ Selector("Função", self.create_functions),
77
+ Selector("CLI", self.create_cli),
78
+ Selector("Input", self.create_inputs),
79
+ Selector("Input sequencial", self.create_sequence_inputs)
80
+ ]
81
+ self.current_file = {}
82
+
83
+
84
+ @Controller.clear_terminal
85
+ def show_selector(self):
86
+ if not hasattr(self, 'tui'):
87
+ self.tui = TUI([self.file, {"":f"Selecione o tipo de parâmetro que você quer analisar para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}. Digite o número correspondente e aperte ENTER."}, {}], max_line=50)
88
+
89
+ for index, selector in enumerate(self.selectors):
90
+ self.tui.contents[2][f"{Style.BRIGHT}{Fore.CYAN}[{index + 1}]"] = f"{Fore.GREEN if selector.selected else Fore.WHITE}{selector.name}"
91
+
92
+ self.tui.show()
93
+
94
+
95
+ def index_is_selectable(self, index: int) -> bool:
96
+ try:
97
+ index = int(index)
98
+ available_selectors = len(self.selectors)
99
+ if index <= available_selectors and index >= 1:
100
+ return True
101
+ else:
102
+ return False
103
+ except ValueError:
104
+ return False
105
+
106
+
107
+ def get_selectors(self):
108
+ self.show_selector()
109
+ selecteds = 0
110
+ while True:
111
+ index = Controller.required_question(input)(Controller.required_text("> "))
112
+ if self.index_is_selectable(index):
113
+ self.selectors[int(index) - 1].selected = True
114
+ selecteds += 1
115
+ break
116
+
117
+ self.show_selector()
118
+ while True:
119
+ index = input(Controller.optional_text("> "))
120
+ if index.strip() != "":
121
+ if self.index_is_selectable(index):
122
+ index = int(index)
123
+ self.selectors[index - 1].selected = not self.selectors[index - 1].selected
124
+ if self.selectors[index - 1].selected:
125
+ selecteds += 1
126
+ else:
127
+ selecteds -= 1
128
+ self.show_selector()
129
+ else:
130
+ print(Controller.required_text("Indice inválido."))
131
+ else:
132
+ if selecteds == 0:
133
+ print(Controller.required_text("Pelo menos um item deve ser selecionado."))
134
+ else:
135
+ return
136
+
137
+
138
+ def create_structure(self):
139
+ if not "STRUCTURE" in self.current_file:
140
+ self.current_file["STRUCTURE"] = {}
141
+ return True
142
+
143
+
144
+ @Controller.clear_terminal
145
+ def create_classes(self):
146
+ self.create_structure()
147
+ if not "CLASSES" in self.current_file["STRUCTURE"]:
148
+ self.current_file["STRUCTURE"]["CLASSES"] = []
149
+
150
+ print(f"Iniciando registro de classes para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
151
+ while True:
152
+ current_class = {}
153
+ class_name = Controller.required_question(input)(Controller.required_text("Nome da classe: "))
154
+ current_class["name"] = class_name
155
+
156
+ is_initialized = Controller.required_question(input)(Controller.required_text("A classe possui um método __init__? (s/n): "))
157
+ if is_initialized.strip()[0].lower() == "s":
158
+ current_class["initializer"] = {}
159
+ current_class["initialized"] = True
160
+ while True:
161
+ print("Digite os parâmetros do método __init__ (um por vez). Para parar, aperte ENTER sem digitar nada: ")
162
+ inputs = Controller.sequential_question(input)(Controller.optional_text("> "))
163
+ current_class["initializer"]["input"] = inputs if inputs else []
164
+
165
+ class_info = input(Controller.optional_text("Descrição da validação (instancialização da classe): "))
166
+ if class_info.strip() != "":
167
+ current_class["initializer"]["info"] = class_info
168
+ break
169
+
170
+ while True:
171
+ add_method = Controller.required_question(input)(Controller.required_text("Deseja adicionar um método à classe? (s/n): "))
172
+ if add_method[0].lower() == "s":
173
+ if not "methods" in current_class:
174
+ current_class["methods"] = []
175
+ method_data = {}
176
+
177
+ method_name = Controller.required_question(input)(Controller.required_text("Nome do método: "))
178
+ method_data["name"] = method_name
179
+
180
+ print("Digite os parâmetros do método (um por vez). Para parar, aperte ENTER sem digitar nada: ")
181
+ method_inputs = Controller.sequential_question(input)(Controller.optional_text("> "))
182
+ method_data["input"] = method_inputs if method_inputs else []
183
+
184
+ is_static = Controller.required_question(input)(Controller.required_text("O método é estático? (s/n): "))
185
+ method_data["static"] = True if is_static.strip()[0].lower() == "s" else False
186
+
187
+ expected_output = input(Controller.optional_text("Output esperado: "))
188
+ method_data["expected"] = expected_output if expected_output else ""
189
+
190
+ method_info = input(Controller.optional_text("Descrição da validação: "))
191
+ if method_info.strip() != "":
192
+ method_data["info"] = method_info
193
+ current_class["methods"].append(method_data)
194
+ else:
195
+ break
196
+
197
+ self.current_file["STRUCTURE"]["CLASSES"].append(current_class)
198
+ print(f"Classe {Style.BRIGHT}{Fore.CYAN}{class_name}{Style.RESET_ALL} adicionada com sucesso!")
199
+ new_class = Controller.required_question(input)(Controller.required_text("Deseja adicionar outra classe? (s/n): "))
200
+ if new_class.strip()[0].lower() != "s":
201
+ break
202
+
203
+
204
+
205
+ @Controller.clear_terminal
206
+ def create_functions(self):
207
+ self.create_structure()
208
+ if not "FUNCTIONS" in self.current_file["STRUCTURE"]:
209
+ self.current_file["STRUCTURE"]["FUNCTIONS"] = []
210
+
211
+ print(f"Iniciando registro de funções para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
212
+ while True:
213
+ current_function = {}
214
+ function_name = Controller.required_question(input)(Controller.required_text("Nome da função: "))
215
+ current_function["name"] = function_name
216
+
217
+ print("Bateria de testes da função")
218
+ current_function["runs"] = []
219
+ while True:
220
+ current_run = {}
221
+
222
+ print("Digite os inputs necessários para testar a função. Para parar, aperte ENTER sem digitar nada.")
223
+ inputs = Controller.sequential_question(input)(Controller.optional_text("> "))
224
+ current_run["input"] = inputs if inputs else []
225
+
226
+ expected = input(Controller.optional_text("Output esperado: "))
227
+ current_run["expected"] = expected if expected else ""
228
+
229
+ info = input(Controller.optional_text("Descrição da validação: "))
230
+ if info.strip() != "":
231
+ current_run["info"] = info
232
+
233
+ current_function["runs"].append(current_run)
234
+ new_run = Controller.required_question(input)(Controller.required_text("Deseja adicionar outra bateria de testes? (s/n): "))
235
+ if new_run.strip()[0].lower() != "s":
236
+ break
237
+
238
+ self.current_file["STRUCTURE"]["FUNCTIONS"].append(current_function)
239
+ print(f"Função {Style.BRIGHT}{Fore.CYAN}{function_name}{Style.RESET_ALL} adicionada com sucesso!")
240
+
241
+ new_class = Controller.required_question(input)(Controller.required_text("Deseja adicionar outra função? (s/n): "))
242
+ if new_class.strip()[0].lower() != "s":
243
+ break
244
+
245
+
246
+
247
+ @Controller.clear_terminal
248
+ def create_cli(self):
249
+ self.current_file["CLI"] = []
250
+ print(f"Iniciando registro de CLI para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
251
+ while True:
252
+ current_cli = {}
253
+ inputs = input(Controller.optional_text("Argumentos do comando: "))
254
+ current_cli["input"] = inputs if inputs else ""
255
+
256
+ expected = input(Controller.optional_text("Output esperado: "))
257
+ current_cli["expected"] = expected if expected else ""
258
+
259
+ info = input(Controller.optional_text("Descrição da validação: "))
260
+ if info.strip() != "":
261
+ current_cli["info"] = info
262
+
263
+ self.current_file["CLI"].append(current_cli)
264
+ print(f"{Style.BRIGHT}{Fore.CYAN}Comando registrado com sucesso!")
265
+ new_run = Controller.required_question(input)(Controller.required_text("Deseja adicionar outro comando CLI? (s/n): "))
266
+ if new_run.strip()[0].lower() != "s":
267
+ break
268
+
269
+
270
+ @Controller.clear_terminal
271
+ def create_inputs(self):
272
+ self.current_file["INPUTS"] = []
273
+ print(f"Iniciando registro de input simples para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
274
+ while True:
275
+ current_input = {}
276
+ text_input = input(Controller.optional_text("Input: "))
277
+ current_input["input"] = text_input
278
+
279
+ expected = input(Controller.optional_text("Expected Output: "))
280
+ current_input["expected"] = expected
281
+
282
+ info = input(Controller.optional_text("Descrição da validação: "))
283
+ if info.strip() != "":
284
+ current_input["info"] = info
285
+
286
+ self.current_file["INPUTS"].append(current_input)
287
+ print(f"{Style.BRIGHT}{Fore.CYAN}Input registrado com sucesso!")
288
+ new_run = Controller.required_question(input)(Controller.required_text("Deseja adicionar outro input simples? (s/n): "))
289
+ if new_run.strip()[0].lower() != "s":
290
+ break
291
+
292
+
293
+ @Controller.clear_terminal
294
+ def create_sequence_inputs(self):
295
+ print(f"Iniciando registro de inputs sequenciais para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
296
+ self.current_file["SEQUENCE_INPUTS"] = []
297
+ while True:
298
+ current_seqinput = {}
299
+ print("Aperte ENTER sem digitar nada para parar de registrar inputs.")
300
+ inputs = Controller.sequential_question(input)(Controller.optional_text("> "))
301
+ current_seqinput["input"] = inputs if inputs else []
302
+
303
+ print("Digite os outputs esperados. Aperte ENTER sem digitar nada para parar.")
304
+ expected = Controller.sequential_question(input)(Controller.optional_text("> "))
305
+ current_seqinput["expected"] = expected if expected else []
306
+
307
+ info = input(Controller.optional_text("Descrição da validação: "))
308
+ if info.strip() != "":
309
+ current_seqinput["info"] = info
310
+
311
+ self.current_file["SEQUENCE_INPUTS"].append(current_seqinput)
312
+ print(f"{Style.BRIGHT}{Fore.CYAN}Input sequencial registrado com sucesso!")
313
+ new_run = Controller.required_question(input)(Controller.required_text("Deseja adicionar outro input sequencial? (s/n): "))
314
+ if new_run.strip()[0].lower() != "s":
315
+ break
316
+
317
+
318
+ def purge_unselected(self):
319
+ selected = []
320
+ for selector in self.selectors:
321
+ if selector.selected:
322
+ selected.append(selector)
323
+ self.selectors = selected
324
+
325
+
326
+ def run_selected(self):
327
+ for selector in self.selectors:
328
+ selector.action()
329
+
330
+
331
+ def show_progress(self):
332
+ TUI([f"{Fore.CYAN}{self.file}", *({f"{Fore.CYAN}[{index+1}]": f"{Fore.GREEN}{key}{Style.RESET_ALL}\n{self.current_file[key]}"} for index, key in enumerate(self.current_file.keys()))], max_line=50).show()
333
+
334
+
335
+ def start(self):
336
+ self.get_selectors()
337
+ self.purge_unselected()
338
+ self.run_selected()
339
+ self.show_progress()
340
+
341
+
342
+ class Builder:
343
+ default_info =[
344
+ f"{Fore.YELLOW}{Style.BRIGHT}Gerador de {settings.get_config("answers_file_name")}",
345
+ f"{Style.DIM}Seu progresso vai ficar aqui.",
346
+ {
347
+ f"{Style.BRIGHT}{Fore.RED}Informações importantes:": "",
348
+ f"{Style.BRIGHT}{Fore.CYAN}1.": "Leia atentamente a todos os avisos e instruções antes de prosseguir.",
349
+ f"{Style.BRIGHT}{Fore.CYAN}2.": f"Inputs {Fore.RED}VERMELHOS{Style.RESET_ALL} são obrigatórios, enquanto inputs {Fore.YELLOW}AMARELOS{Style.RESET_ALL} são opcionais.",
350
+ f"{Style.BRIGHT}{Fore.CYAN}3.": f"Alguns inputs exigem uma sequência de informções. Esses serão indicados com {Fore.GREEN}setas coloridas (>){Style.RESET_ALL}. Quando quiser parar de adicionar informações, basta apertar {Style.BRIGHT}ENTER{Style.RESET_ALL} sem digitar nada enquanto a seta estiver {Fore.YELLOW}AMARELA (>){Style.RESET_ALL}.",
351
+ }
352
+ ]
353
+
354
+ @staticmethod
355
+ def confirm_yield():
356
+ input("Pressione ENTER para continuar...")
357
+
358
+ def __init__(self):
359
+ self.build = {}
360
+ self.info = TUI(self.default_info, max_line=80)
361
+
362
+ @Controller.clear_terminal
363
+ def show_info(self):
364
+ self.info.show()
365
+
366
+ def add_block(self, name: str, content: str):
367
+ self.build[name] = content
368
+
369
+ def show_progress(self, name: str, text: str):
370
+ progress = self.info.contents[1]
371
+ if not isinstance(progress, dict):
372
+ progress = {}
373
+
374
+ progress[f"{Style.BRIGHT}{name}"] = text
375
+ self.info.contents[1] = progress
376
+
377
+ self.show_info()
378
+
379
+ def get_files(self):
380
+ files = []
381
+
382
+ def format_filename(filename: str) -> str:
383
+ if not filename.endswith(".py"):
384
+ filename += ".py"
385
+ return filename
386
+
387
+ print(f"Vamos começar nomeando os arquivos que você quer analisar. O primeiro arquivo é obrigatório, então digite o nome dele e aperte ENTER.\n{Fore.YELLOW}OBSERVAÇÃO: A extensão .py será adicionada automaticamente.")
388
+ files.append(format_filename(Controller.required_question(input)(f"{Controller.required_text('Nome do primeiro arquivo: ')}")))
389
+ print("Perfeito! Agora, se quiser adicionar mais arquivos, digite o nome deles um por vez. Para parar aperte ENTER sem digitar nada.")
390
+ files.extend(list(map(format_filename, Controller.sequential_question(input)(f"{Controller.optional_text('> ')}"))))
391
+
392
+ return list(dict.fromkeys(files))
393
+
394
+
395
+ @Controller.clear_terminal
396
+ def save_file(self):
397
+ assignment_name = Controller.required_question(input)(f"{Controller.required_text('Nome da atividade: ')}")
398
+ save_path = Path.cwd() / assignment_name
399
+
400
+ print(f"O arquivo \"correcao.json\" será salvo em {save_path}.")
401
+ change_path = input(f"{Controller.optional_text('Deseja alterar o caminho? (s/n): ')}")
402
+
403
+ if change_path and change_path.strip()[0].lower() == "s":
404
+ user_input = Controller.required_question(input)(f"{Controller.required_text('Novo caminho: ')}")
405
+ save_path = Path(user_input)
406
+
407
+ file_path = save_path / "correcao.json"
408
+ file_path.parent.mkdir(parents=True, exist_ok=True)
409
+
410
+ with open(file_path, "w", encoding="utf-8") as file:
411
+ json.dump(self.build, file, indent=4, ensure_ascii=False)
412
+
413
+ print(f"Arquivo salvo em {file_path}.")
414
+
415
+ def start(self):
416
+ self.show_info()
417
+
418
+ files = self.get_files()
419
+ self.add_block("files", files)
420
+ self.show_progress("Arquivos selecionados: ", f"{'; '.join(files)}.")
421
+ self.confirm_yield()
422
+
423
+ Controller.clear_command()
424
+ for file in files:
425
+ fb = FileBuilder(file)
426
+ fb.start()
427
+ self.add_block(file, fb.current_file)
428
+
429
+ self.confirm_yield()
430
+
431
+ self.save_file()
verifica/check.py ADDED
@@ -0,0 +1,218 @@
1
+ from colorama import Fore, Style
2
+ from pathlib import Path
3
+ import subprocess
4
+ import traceback
5
+ import logging
6
+ import sys
7
+ from types import SimpleNamespace
8
+
9
+ from . import imports_tester
10
+
11
+ class Checker:
12
+
13
+ def __init__(self, exercises_path: str, answers: dict):
14
+ """Atribui os parâmetros passados para o objeto
15
+
16
+ :param exercises_path: Caminho da pasta com arquivos do exercício
17
+ :param answers: Dicionário de respostas no formato apropriado
18
+ """
19
+ self.exercises_path = exercises_path
20
+ self.answers = answers
21
+ self.roadmap = []
22
+ self.logger = logging.getLogger(__name__)
23
+
24
+ def __file_exists(self, file_path: str) -> bool:
25
+ """Checa se um arquivo existe em determinado caminho
26
+
27
+ :param file_path: Caminho do arquivo
28
+ :returns: Verdadeiro caso exista
29
+ :rtype: bool
30
+ """
31
+ return Path(self.exercises_path / file_path).is_file()
32
+
33
+ def setup_roadmap(self) -> bool:
34
+ """Popula a lista roadmap com uma sequência de testes a serem realizados"""
35
+ try:
36
+ self.logger.info(f"Configurando roadmap")
37
+ for file in self.answers["files"]:
38
+ current_file_path = Path(self.exercises_path) / file
39
+ self.logger.info(f"Inicindo detecções para o arquivo '{file}'")
40
+
41
+ if len(self.answers[file]) == 0:
42
+ raise ValueError(f"O arquivo de correção para '{file}' não possui características a serem testadas")
43
+
44
+ self.logger.info(file)
45
+ self.roadmap.append({
46
+ "info": f"'{file}' existe",
47
+ "args": [self, file],
48
+ "action": Checker.__file_exists
49
+ })
50
+
51
+ for check_step, subsequent_steps in self.answers[file].items():
52
+ self.logger.info(f"CARACTERÍSTICA DETECTADA: {check_step}")
53
+ if check_step == "CLI":
54
+ for command in subsequent_steps:
55
+ self.logger.info(f"ADICIONANDO COMANDO CLI: {file} {command['input']}")
56
+ self.roadmap.append({
57
+ "info": command.get("info", f"comando '{command['input']}' retorna '{command['expected']}'"),
58
+ "args": [self, current_file_path, command["input"].split(" "), command["expected"]],
59
+ "action": Checker.test_CLI
60
+ })
61
+ elif check_step == "STRUCTURE":
62
+ try:
63
+ importedFile = imports_tester.Imported(current_file_path)
64
+ except Exception as e:
65
+ self.logger.warning(f"Falha ao importar o arquivo '{file}': {e}")
66
+ importedFile = SimpleNamespace(module=None, logger=self.logger)
67
+ if subsequent_steps.get("CLASSES"):
68
+ self.logger.info(f"CLASSES DETECTADAS")
69
+ for class_info in subsequent_steps["CLASSES"]:
70
+ self.logger.info(f"ADICIONANDO CLASSE {class_info['name']}")
71
+ is_initialized = class_info.get("initialized", False)
72
+ has_methods = class_info.get("methods", []) != []
73
+ currentClass = imports_tester.ClassTester(importedFile, class_info["name"], class_info["methods"] if has_methods else [], is_initialized)
74
+ self.roadmap.append({
75
+ "info": class_info.get("info", f"classe {class_info['name']} existe e possui os métodos esperados"),
76
+ "args": [currentClass],
77
+ "action": imports_tester.ClassTester.get_existance
78
+ })
79
+ if is_initialized:
80
+ self.roadmap.append({
81
+ "info": class_info.get("info", f"classe {class_info['name']} pode ser instanciada"),
82
+ "args": [currentClass, *class_info["initializer"]["input"]],
83
+ "action": imports_tester.ClassTester.initialize_instance
84
+ })
85
+ if has_methods:
86
+ for method_info in class_info["methods"]:
87
+ self.roadmap.append({
88
+ "info": method_info.get("info", f"{method_info['name']}({', '.join(map(str, method_info['input']))}) retorna {method_info['expected']}"),
89
+ "args": [method_info["name"], method_info.get("static", False), method_info["input"], method_info["expected"]],
90
+ "action": currentClass.test_method
91
+ })
92
+ if subsequent_steps.get("FUNCTIONS"):
93
+ self.logger.info(f"FUNÇÕES DETECTADAS")
94
+ for function_info in subsequent_steps["FUNCTIONS"]:
95
+ self.logger.info(f"ADICIONANDO FUNÇÃO {function_info['name']}")
96
+ currentFunction = imports_tester.FunctionTester(importedFile, function_info["name"])
97
+ self.roadmap.append({
98
+ "info": function_info.get("info", f"função {function_info['name']} existe"),
99
+ "args": [currentFunction],
100
+ "action": imports_tester.FunctionTester.get_existance
101
+ })
102
+ for run in function_info["runs"]:
103
+ self.logger.info(f"ADICIONANDO RUN {function_info['name']}({', '.join(map(str, run['input']))})")
104
+ self.roadmap.append({
105
+ "info": run.get("info", f"função {function_info['name']}({', '.join(map(str, run['input']))}) retorna {run['expected']}"),
106
+ "args": [currentFunction, run["input"], run["expected"]],
107
+ "action": imports_tester.FunctionTester.test
108
+ })
109
+ elif check_step == "INPUTS":
110
+ for input_info in subsequent_steps:
111
+ self.logger.info(f"ADICIONANDO INPUT {input_info['input']}")
112
+ self.roadmap.append({
113
+ "info": input_info.get("info", f"input '{input_info['input']}' retorna '{input_info['expected']}'"),
114
+ "args": [current_file_path, input_info["input"], input_info["expected"]],
115
+ "action": self.test_INPUT
116
+ })
117
+ elif check_step == "SEQUENCE_INPUTS":
118
+ for sequence_input_info in subsequent_steps:
119
+ self.logger.info(f"ADICIONANDO SEQUENCE_INPUTS {sequence_input_info['input']}")
120
+ self.roadmap.append({
121
+ "info": sequence_input_info.get("info", f"input '{', '.join(sequence_input_info['input'])}' retorna '{', '.join(sequence_input_info['expected'])}'"),
122
+ "args": [current_file_path, sequence_input_info["input"], sequence_input_info["expected"]],
123
+ "action": self.test_SEQUENCE_INPUT
124
+ })
125
+ else:
126
+ raise ValueError(f"Característica desconhecida '{check_step}' no arquivo de correção")
127
+ except (ValueError, KeyError) as e:
128
+ self.logger.error(f"Estrutura do arquivo de correção inválida para o arquivo '{file}'.\nDetalhes: {e}")
129
+ return False
130
+ except Exception as e:
131
+ self.logger.error(f"Erro ao configurar roadmap para o arquivo '{file}'.")
132
+ self.logger.error(f"Detalhes do erro: {e}\n{traceback.format_exc()}")
133
+ return False
134
+
135
+ return True
136
+
137
+
138
+ def test_CLI(self, file_path: str, input_args: list[str], expected_output: str) -> bool:
139
+ try:
140
+ result = subprocess.run([sys.executable, file_path] + input_args, capture_output=True, text=True)
141
+ self.logger.warning(f"Saída do comando '{' '.join(input_args)}': {result.stdout.strip()}")
142
+ return expected_output in result.stdout.strip()
143
+ except subprocess.CalledProcessError as e:
144
+ self.logger.warning(f"O script '{input_args[0]}' falhou com o código de saída {e.returncode}")
145
+ self.logger.warning(f"Detalhes do erro: {e.stderr}")
146
+ return False
147
+
148
+ def test_INPUT(self, file: str, input_data: str, expected_output: str) -> bool:
149
+ try:
150
+ result = subprocess.run(
151
+ [sys.executable, file],
152
+ input=input_data,
153
+ capture_output=True,
154
+ text=True
155
+ )
156
+ return expected_output in result.stdout.strip()
157
+ except subprocess.CalledProcessError as e:
158
+ self.logger.warning(f"A ação do input '{input_data}' falhou com o código de saída {e.returncode}")
159
+ self.logger.warning(f"Detalhes do erro: {e.stderr}")
160
+ return False
161
+
162
+ def test_SEQUENCE_INPUT(self, file: str, input_data: list[str], expected_output: list[str]) -> bool:
163
+ try:
164
+ result = subprocess.run(
165
+ [sys.executable, file],
166
+ input=("\n").join(input_data),
167
+ capture_output=True,
168
+ text=True
169
+ )
170
+ output = result.stdout.strip()
171
+ return all(palavra in output for palavra in expected_output)
172
+ except subprocess.CalledProcessError as e:
173
+ self.logger.warning(f"A ação do input múltiplo '{input_data}' falhou com o código de saída {e.returncode}")
174
+ self.logger.warning(f"Detalhes do erro: {e.stderr}")
175
+ return False
176
+
177
+ def make_result_message(self, result: bool, info: str) -> str:
178
+ """Gera uma mensagem de resultado formatada com cores
179
+
180
+ :param result: Resultado do teste
181
+ :param info: Informação sobre o teste
182
+ :returns: Mensagem formatada
183
+ :rtype: str
184
+ """
185
+ colors = {
186
+ True: Fore.GREEN,
187
+ False: Fore.RED
188
+ }
189
+ return f"{colors[result]}{Style.BRIGHT}{':)' if result else ':('}{Style.RESET_ALL} {colors[result]}{info}{Style.RESET_ALL}"
190
+
191
+ def run_roadmap(self) -> list:
192
+ """Executa os testes do roadmap e retorna uma lista de resultados
193
+
194
+ :returns: Lista de resultados dos testes
195
+ :rtype: list
196
+ """
197
+ results = []
198
+ for step in self.roadmap:
199
+ try:
200
+ self.logger.debug(step["args"])
201
+ result = step["action"](*step["args"])
202
+ except Exception as e:
203
+ result = False
204
+ self.logger.warning(f"Erro ao executar o teste '{step['info']}': {e}\n{traceback.format_exc()}")
205
+
206
+ results.append(self.make_result_message(result, step["info"]))
207
+
208
+ return results
209
+
210
+ def show_results(self, results: list) -> None:
211
+ """Exibe os resultados dos testes no console
212
+
213
+ :param results: Lista de resultados dos testes
214
+ """
215
+ if self.answers.get("description"):
216
+ print(f"{self.answers['description']}")
217
+ for result in results:
218
+ print(result)
verifica/config.py ADDED
@@ -0,0 +1,80 @@
1
+ from platformdirs import user_config_dir
2
+ from importlib.metadata import version, PackageNotFoundError
3
+ from time import time
4
+ import json
5
+ import os
6
+
7
+
8
+ class Config:
9
+ _instance = None
10
+
11
+ def __new__(cls, *args, **kwargs):
12
+ if cls._instance is None:
13
+ cls._instance = super().__new__(cls, *args, **kwargs)
14
+ return cls._instance
15
+
16
+ def __init__(self):
17
+ if hasattr(self, "_initialized"):
18
+ return
19
+ self._initialized = True
20
+
21
+ config_dir = user_config_dir("verifica")
22
+ os.makedirs(config_dir, exist_ok=True)
23
+
24
+ self.config_dir = config_dir
25
+ self.config_path = os.path.join(config_dir, "config.toml")
26
+ self.__check_config_state()
27
+
28
+ def __save_keys(self, data: dict):
29
+ with open(self.config_path, "w", encoding="utf-8") as f:
30
+ json.dump(data, f, indent=4)
31
+
32
+ def __read_keys(self) -> dict:
33
+ if not self.config_path or not os.path.exists(self.config_path):
34
+ return {}
35
+ with open(self.config_path, "r", encoding="utf-8") as f:
36
+ try:
37
+ return json.load(f)
38
+ except json.decoder.JSONDecodeError:
39
+ return {}
40
+
41
+ def __check_config_state(self):
42
+ defaultConfig = {
43
+ "version": self.get_version(),
44
+ "url": "https://raw.githubusercontent.com",
45
+ "answers_file_name": "correcao.json"
46
+ }
47
+
48
+ current_config = self.__read_keys()
49
+ if len(current_config) == 0:
50
+ self.__save_keys(defaultConfig)
51
+ else:
52
+ if current_config.get("version") != defaultConfig.get("version"):
53
+ self.update_config(defaultConfig, defaultConfig.get("version"))
54
+
55
+ def get_version(self):
56
+ try:
57
+ __version__ = version("verifica")
58
+ except PackageNotFoundError:
59
+ __version__ = f"indev-{time()}"
60
+ return __version__
61
+
62
+ def update_config(self, new_data: dict, version: str = None):
63
+ current_config = self.__read_keys()
64
+ if version:
65
+ current_config["version"] = version
66
+ new_data.update(current_config)
67
+ self.__save_keys(new_data)
68
+
69
+ def get_config(self, key: str = None) -> dict:
70
+ config = self.__read_keys()
71
+ if key:
72
+ return config.get(key, "")
73
+ return config
74
+
75
+ def set_config(self, key: str, value):
76
+ config = self.__read_keys()
77
+ config[key] = value
78
+ self.update_config(config)
79
+
80
+ settings = Config()
verifica/fetcher.py ADDED
@@ -0,0 +1,75 @@
1
+ from pathlib import Path
2
+ from urllib.error import HTTPError, URLError
3
+ from urllib.request import Request, urlopen
4
+ import tempfile
5
+ import logging
6
+ import json
7
+
8
+ from .config import settings
9
+
10
+ class Fetcher:
11
+ def __init__(self, path, local: bool = False):
12
+ self.exercise = path
13
+ self.local = local
14
+ if not local:
15
+ self.base_url = settings.get_config("url")
16
+ self.remote_path = f"{self.exercise.strip()}/{settings.get_config('answers_file_name')}"
17
+ self.logger = logging.getLogger(__name__)
18
+
19
+ def __str__(self):
20
+ return f"Fetcher(url=\"{self._build_url()}\")"
21
+
22
+ def _build_url(self):
23
+ return f"{self.base_url}/{self.remote_path}"
24
+
25
+ def fetch(self) -> str:
26
+ """Baixa o arquivo de respostas e salva em uma pasta temporária
27
+
28
+ :returns: O caminho do arquivo salvo
29
+ :rtype: str
30
+ :raises RuntimeError: Se não for possível buscar o exercício
31
+ """
32
+ try:
33
+ self.logger.info("Baixando arquivo de correção...")
34
+ request = Request(self._build_url())
35
+
36
+ request.add_header("Cache-Control", "no-cache, no-store, must-revalidate")
37
+ request.add_header("Pragma", "no-cache")
38
+ request.add_header("Expires", "0")
39
+
40
+ with urlopen(request) as response:
41
+ content = response.read().decode("utf-8")
42
+ except (HTTPError, URLError) as error:
43
+ self.logger.error(f"Falha ao buscar o arquivo de correção '{self.exercise}': {error}")
44
+ raise RuntimeError(f"Falha ao buscar o arquivo de correção '{self.exercise}'") from error
45
+
46
+ self.file = tempfile.NamedTemporaryFile(mode='w+t', prefix='verifica-', suffix='.json', encoding='utf-8')
47
+ self.file.write(content)
48
+
49
+ return self.file.name
50
+
51
+ def get_file(self):
52
+ if self.local:
53
+ local_path = Path(self.exercise) / settings.get_config("answers_file_name")
54
+ if not local_path.is_file():
55
+ raise FileNotFoundError(f"O arquivo de respostas não foi encontrado em '{local_path}'")
56
+ self.file = open(local_path, 'r', encoding='utf-8')
57
+ return str(local_path)
58
+
59
+
60
+ def get_content(self):
61
+ if not self.file:
62
+ raise ValueError("o arquivo de correção não existe")
63
+
64
+ self.file.seek(0)
65
+ return self.file.read()
66
+
67
+ def get_decoded_json(self):
68
+ if not self.file:
69
+ raise ValueError("o arquivo de correção não existe")
70
+
71
+ return json.loads(self.get_content())
72
+
73
+ def cleanup(self):
74
+ if self.file != None:
75
+ self.file.close()
@@ -0,0 +1,146 @@
1
+ import importlib.util
2
+ import logging
3
+ import traceback
4
+ from contextlib import redirect_stderr, redirect_stdout
5
+ from io import StringIO
6
+
7
+ def _call_with_captured_output(callable_obj, *args, **kwargs):
8
+ stdout_buffer = StringIO()
9
+ stderr_buffer = StringIO()
10
+ with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
11
+ result = callable_obj(*args, **kwargs)
12
+
13
+ return result, stdout_buffer.getvalue().strip(), stderr_buffer.getvalue().strip()
14
+
15
+ class Imported:
16
+ def __init__(self, module_path: str):
17
+ self.logger = logging.getLogger(__name__)
18
+ self.module = self.__import_module(module_path)
19
+
20
+ def __import_module(self, module_path: str):
21
+ """Importa um módulo Python a partir de um caminho de arquivo
22
+
23
+ :param module_path: Caminho do arquivo do módulo
24
+ :returns: O módulo importado
25
+ :rtype: module
26
+ :raises ImportError: Se não for possível importar o módulo
27
+ """
28
+ try:
29
+ spec = importlib.util.spec_from_file_location("module_custom", module_path)
30
+ if spec is None or spec.loader is None:
31
+ raise ImportError(f"Não foi possível criar o carregador do módulo '{module_path}'")
32
+
33
+ module = importlib.util.module_from_spec(spec)
34
+ with redirect_stdout(StringIO()), redirect_stderr(StringIO()):
35
+ spec.loader.exec_module(module)
36
+ return module
37
+ except Exception as e:
38
+ self.logger.warning(f"Falha ao importar o módulo '{module_path}': {e}")
39
+ raise ImportError(f"Falha ao importar o módulo '{module_path}'") from e
40
+
41
+ class ClassTester:
42
+ def __init__(self, import_info: Imported, class_name: str, methods: list = [], initialized: bool = False):
43
+ self.import_info = import_info
44
+ self.class_name = class_name
45
+ self.methods = methods
46
+ self.initialized = initialized
47
+ self.exists = self.check_existance()
48
+ self.instance = None
49
+
50
+ def check_existance(self):
51
+ """Testa se a classe existe e se possui os métodos esperados"""
52
+ cls = getattr(self.import_info.module, self.class_name, None)
53
+ if cls is None:
54
+ self.import_info.logger.warning(f"A classe '{self.class_name}' não foi encontrada no módulo.")
55
+ return False
56
+
57
+ for method in self.methods:
58
+ if not hasattr(cls, method["name"]):
59
+ self.import_info.logger.warning(f"O método '{method['name']}' não foi encontrado na classe '{self.class_name}'.")
60
+ return False
61
+
62
+ return True
63
+
64
+ def initialize_instance(self, *args) -> bool:
65
+ """Inicializa uma instância da classe com os argumentos fornecidos"""
66
+ if not self.exists:
67
+ self.import_info.logger.warning(f"A classe '{self.class_name}' não existe ou não possui os métodos esperados.")
68
+ return False
69
+
70
+ cls = getattr(self.import_info.module, self.class_name, None)
71
+ try:
72
+ self.instance = cls(*args)
73
+ except Exception as e:
74
+ self.import_info.logger.warning(f"Falha ao inicializar a instância da classe '{self.class_name}': {e}")
75
+ return False
76
+
77
+ return True
78
+
79
+ def get_existance(self):
80
+ """Retorna se a classe existe e possui os métodos esperados"""
81
+ return self.exists
82
+
83
+ def test_method(self, method_name: str, static: bool, input_args: list, expected_output):
84
+ """Testa se um método da classe retorna o valor esperado"""
85
+ if not self.exists:
86
+ self.import_info.logger.warning(f"A classe '{self.class_name}' não existe ou não possui os métodos esperados.")
87
+ return False
88
+
89
+ try:
90
+ cls = getattr(self.import_info.module, self.class_name, None)
91
+ method = getattr(cls, method_name, None)
92
+
93
+ if not static and self.instance is not None:
94
+ result, captured_stdout, _ = _call_with_captured_output(method, self.instance, *input_args)
95
+ else:
96
+ result, captured_stdout, _ = _call_with_captured_output(method, *input_args)
97
+
98
+ observed_output = result if result is not None else captured_stdout
99
+ if observed_output != expected_output and str(observed_output) != str(expected_output):
100
+ self.import_info.logger.warning(f"O método '{method_name}' retornou '{observed_output}', mas era esperado '{expected_output}'.")
101
+ return False
102
+ except Exception as e:
103
+ self.import_info.logger.warning(f"Falha ao testar o método '{method_name}': {e}")
104
+ self.import_info.logger.warning(f"Traceback: {traceback.format_exc()}")
105
+ return False
106
+
107
+ return True
108
+
109
+ class FunctionTester:
110
+ def __init__(self, import_info: Imported, function_name: str):
111
+ self.import_info = import_info
112
+ self.function_name = function_name
113
+ self.exists = self.check_existance()
114
+
115
+ def check_existance(self):
116
+ """Testa se a função existe"""
117
+ func = getattr(self.import_info.module, self.function_name, None)
118
+ if func is None:
119
+ self.import_info.logger.warning(f"A função '{self.function_name}' não foi encontrada no módulo.")
120
+ return False
121
+
122
+ return True
123
+
124
+ def get_existance(self):
125
+ """Retorna se a função existe"""
126
+ return self.exists
127
+
128
+ def test(self, input_args: list, expected_output):
129
+ """Testa se a função retorna o valor esperado"""
130
+ if not self.exists:
131
+ self.import_info.logger.info(f"A função '{self.function_name}' não existe.")
132
+ return False
133
+
134
+ try:
135
+ func = getattr(self.import_info.module, self.function_name, None)
136
+ result, captured_stdout, _ = _call_with_captured_output(func, *input_args)
137
+ observed_output = result if result is not None else captured_stdout
138
+ if observed_output != expected_output and str(observed_output) != str(expected_output):
139
+ self.import_info.logger.warning(f"A função '{self.function_name}' retornou '{observed_output}', mas era esperado '{expected_output}'.")
140
+ return False
141
+ except Exception as e:
142
+ self.import_info.logger.warning(f"Falha ao testar a função '{self.function_name}': {e}")
143
+ self.import_info.logger.warning(f"Traceback: {traceback.format_exc()}")
144
+ return False
145
+
146
+ return True
verifica/tui.py ADDED
@@ -0,0 +1,102 @@
1
+ from colorama import Style
2
+ import re
3
+
4
+ # Regex para remover códigos ANSI de cores e formatação
5
+ ANSI_PATTERN = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]")
6
+
7
+ class TUI:
8
+ @staticmethod
9
+ def visible_len(text: str) -> int:
10
+ """Retorna o tamanho de uma string sem caracteres de formatação ANSI"""
11
+ return len(ANSI_PATTERN.sub("", text))
12
+
13
+ def __init__(self, contents: list, show: bool = False, max_line: int = -1, padding: int = 1):
14
+ self.contents = contents
15
+ self.padding = padding
16
+ self.max_line = max_line
17
+ self.min_size = self.__get_longest_message_length() + (padding * 2)
18
+ self.row_size = self.min_size + self.padding
19
+ if show:
20
+ self.show()
21
+
22
+ def __get_longest_message_length(self):
23
+ longest = 0
24
+ for block in self.contents:
25
+ if isinstance(block, dict):
26
+ for k, v in block.items():
27
+ for line in self.__get_paragraphs(k, self.__create_paragraphs(k, v)):
28
+ length = self.visible_len(line)
29
+ if length > longest:
30
+ longest = length
31
+ else:
32
+ length = self.visible_len(block)
33
+ if length > longest:
34
+ longest = length
35
+ return longest
36
+
37
+ def __get_paragraphs(self, key: str, text: str) -> list:
38
+ lines = []
39
+ paragraphs = text.split('\n')
40
+
41
+ lines.append(f"{key}{Style.RESET_ALL} {paragraphs[0]}")
42
+ for paragraph in paragraphs[1:]:
43
+ lines.append(f"{' ' * (self.visible_len(key) + 1)}{paragraph}{Style.RESET_ALL}")
44
+
45
+ return lines
46
+
47
+ def __create_paragraphs(self, key: str, text: str) -> str:
48
+ if self.max_line >= 1:
49
+ paragraphs = []
50
+ words = text.split()
51
+ current_line = ""
52
+
53
+ for word in words:
54
+ if self.visible_len(f"{key}{Style.RESET_ALL} {current_line}") + self.visible_len(word) + 1 <= self.max_line:
55
+ current_line += f"{word} "
56
+ else:
57
+ paragraphs.append(current_line.strip())
58
+ current_line = f"{word} "
59
+
60
+ if current_line:
61
+ paragraphs.append(current_line.strip())
62
+
63
+ return '\n'.join(paragraphs)
64
+ return [text]
65
+
66
+ def __create_top_border(self):
67
+ return f"╔{'═' * self.row_size}╗"
68
+
69
+ def __create_internal_border(self):
70
+ return f"╠{'═' * self.row_size}╣"
71
+
72
+ def __create_bottom_border(self):
73
+ return f"╚{'═' * self.row_size}╝"
74
+
75
+ def __create_left_border(self):
76
+ return f"║"
77
+
78
+ def __create_right_border(self):
79
+ return f"║"
80
+
81
+ def __create_center_row(self, text: str):
82
+ return f"{self.__create_left_border()}{" "*self.padding}{text.center(self.min_size - self.padding + (len(text) - self.visible_len(text)))}{Style.RESET_ALL}{" "*self.padding}{self.__create_right_border()}"
83
+
84
+ def __create_row(self, text: str):
85
+ return f"{self.__create_left_border()}{" "*self.padding}{text.ljust(self.min_size - self.padding + (len(text) - self.visible_len(text)))}{" "*self.padding}{Style.RESET_ALL}{self.__create_right_border()}"
86
+
87
+ def show(self):
88
+ last_element = len(self.contents) - 1
89
+
90
+ print(f"{self.__create_top_border()}")
91
+ for index, block in enumerate(self.contents):
92
+ if isinstance(block, dict):
93
+ for key, message in self.contents[index].items():
94
+ for line in self.__get_paragraphs(key, self.__create_paragraphs(key, message)):
95
+ print(f"{self.__create_row(line)}")
96
+ else:
97
+ print(f"{self.__create_center_row(block)}")
98
+
99
+ if index != last_element:
100
+ print(f"{self.__create_internal_border()}")
101
+ else:
102
+ print(f"{self.__create_bottom_border()}")
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: verifica
3
+ Version: 1.0.0
4
+ Summary: Uma ferramenta simples para correção de atividades em python via CLI.
5
+ Author-email: Théo Modeneis Ruela <theo.ruela@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/espinafr/Verifica
8
+ Project-URL: Issues, https://github.com/espinafr/Verifica/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: colorama>=0.4.6
15
+ Requires-Dist: platformdirs>=2.5.2
16
+ Dynamic: license-file
17
+
18
+ <div align="center">
19
+ <h1 align="center">Verifica</h1>
20
+ <p align="center">Uma ferramenta simples para correção de atividades em python via CLI.<br>
21
+ <table>
22
+ <tbody>
23
+ <tr>
24
+ <td><a href="https://pypi.org/project/verifica/">Página no Pypi</a></td>
25
+ <td align="right"><a href="https://github.com/espinafr/verifica/issues/new?template=denúncia-de-bug.yml">Reportar bug</a></td>
26
+ </tr>
27
+ <tr>
28
+ <td colspan="2" align="center"><a href="https://github.com/espinafr/verifica/tree/master/docs">Como configurar</a></td>
29
+ </tr>
30
+ </tbody>
31
+ </table>
32
+ </div>
33
+
34
+ ## Sobre o projeto
35
+
36
+ **Verifica** é uma ferramenta simples que permite que professores preparem testes automatizados para corrigir atividades em python, fornecendo aos alunos uma correção instantânea para seus programas antes deles serem enviados oficialmente.
37
+
38
+ ## Como usar
39
+ _Quer saber como criar os arquivos de correção? [Clique aqui](https://github.com/espinafr/verifica/tree/master/docs)_
40
+ 1. Baixe a ferramenta:
41
+ ```bash
42
+ pip install verifica
43
+ ```
44
+
45
+ 2. Faça as atividades propostas com base nas orientações recebidas.
46
+
47
+ 3. No diretório com os arquivos `.py`, execute o comando `python -m verifica` seguido da localização do diretório com os arquivos de correção no github
48
+ ```bash
49
+ python -m verifica usuario/repositorio/branch/localizacao/da/pasta
50
+ ```
51
+
52
+ ## Como compilar
53
+
54
+ Para gerar os arquivos de distribuição do projeto, certifique-se de ter o módulo `build` instalado:
55
+
56
+ ```bash
57
+ pip install build
58
+ ```
59
+
60
+ Em seguida, execute o comando de construção na raiz do projeto:
61
+
62
+ ```bash
63
+ python -m build
64
+ ```
65
+
66
+ Os arquivos gerados (arquivos `.tar.gz` e `.whl`) estarão disponíveis no diretório `dist/`. Use o arquivo `.whl` para distribuição e instalação do pacote via pip.
67
+
68
+ ```bash
69
+ pip install dist/{NOME DO ARQUIVO GERADO}
70
+ ```
71
+
72
+ ## Reconhecimentos
73
+
74
+ Veja o arquivo [ACKNOWLEDGEMENTS](https://github.com/espinafr/verifica/blob/master/ACKNOWLEDGEMENTS.md).
75
+
76
+ ## Licença
77
+
78
+ Veja o arquivo [LICENSE](https://github.com/espinafr/verifica/blob/master/LICENSE).
@@ -0,0 +1,14 @@
1
+ verifica/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ verifica/__main__.py,sha256=HTyOk5udinG7zqsDodcWyZAvQgmMcwkANx8DBUuDRnw,3329
3
+ verifica/builder.py,sha256=QZgMASxalJxgS1wQw-_w_5ZuvuyY90vdMUuzwsndKN0,17917
4
+ verifica/check.py,sha256=KkKBhCsZRXdjuu4Peoa6mga443pTw-LzorhNgCgxlp0,11715
5
+ verifica/config.py,sha256=qVR6hUu4ErmOuBUv-pNAEtNzvjVzInOhzcmbWTRJj0g,2477
6
+ verifica/fetcher.py,sha256=voSwYP2vh1O9epKrMOsCD3D8_gW53297AaWQlVH-x0o,2622
7
+ verifica/imports_tester.py,sha256=ufVG695l7OWsjSMJ-t8thNiDnF1YAI-ootoIKDs9ieo,6489
8
+ verifica/tui.py,sha256=h0PXZVB_YGvKXvw0QR0hILTvroxIESXCClvuCfrOklc,3917
9
+ verifica-1.0.0.dist-info/licenses/LICENSE,sha256=2wrGlJpSJO5h3xtPNKsk-rRndHLvR3Gn-WpZjPv_eSI,1076
10
+ verifica-1.0.0.dist-info/METADATA,sha256=d95CiZ-zwqzec27svCgR7EzxA82It7lpdjZkqrzhTOQ,2734
11
+ verifica-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
+ verifica-1.0.0.dist-info/entry_points.txt,sha256=WRSI-L9wjQbngIA5Ib6vwaskkUFNuDGhhWxtuRhUEQQ,52
13
+ verifica-1.0.0.dist-info/top_level.txt,sha256=ny1Gr7bCRgQGcTe8WfaEAZNUA1_BMhTRxc4cyF8V4Yo,9
14
+ verifica-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ verifica = verifica.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Théo Modeneis Ruela
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ verifica