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 +0 -0
- verifica/__main__.py +85 -0
- verifica/builder.py +431 -0
- verifica/check.py +218 -0
- verifica/config.py +80 -0
- verifica/fetcher.py +75 -0
- verifica/imports_tester.py +146 -0
- verifica/tui.py +102 -0
- verifica-1.0.0.dist-info/METADATA +78 -0
- verifica-1.0.0.dist-info/RECORD +14 -0
- verifica-1.0.0.dist-info/WHEEL +5 -0
- verifica-1.0.0.dist-info/entry_points.txt +2 -0
- verifica-1.0.0.dist-info/licenses/LICENSE +21 -0
- verifica-1.0.0.dist-info/top_level.txt +1 -0
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,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
|