Commit 19861c70afc10a5da4ffd410c22bd40f7493d790
1 parent
c2abd6bf
Exists in
master
and in
1 other branch
Asynchronous run_script_async using asyncio.subprocess.
Asynchronous question generators. Asynchronous correction of textarea questions. Removes 'lines' option in textarea questions (unused in codemirror). The async implementations fixes errors with threading and sqlalchemy.
Showing
10 changed files
with
174 additions
and
68 deletions
Show diff stats
BUGS.md
1 | 1 | |
2 | 2 | # BUGS |
3 | 3 | |
4 | +- quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo) | |
5 | +- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) | |
6 | +- devia mostrar timout para o aluno saber a razao. | |
4 | 7 | - permitir configuracao para escolher entre static files locais ou remotos |
5 | 8 | - sqlalchemy.pool.impl.NullPool: Exception during reset or similar |
6 | 9 | sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. | ... | ... |
aprendizations/knowledge.py
... | ... | @@ -58,7 +58,7 @@ class StudentKnowledge(object): |
58 | 58 | 'level': 0.0, # unlocked |
59 | 59 | 'date': datetime.now() |
60 | 60 | } |
61 | - logger.debug(f'Unlocked "{topic}".') | |
61 | + logger.debug(f'[unlock_topics] Unlocked "{topic}".') | |
62 | 62 | # else: # lock this topic if deps do not satisfy min_level |
63 | 63 | # del self.state[topic] |
64 | 64 | |
... | ... | @@ -68,7 +68,7 @@ class StudentKnowledge(object): |
68 | 68 | # current_question: the current question to be presented |
69 | 69 | # ------------------------------------------------------------------------ |
70 | 70 | async def start_topic(self, topic): |
71 | - logger.debug('StudentKnowledge.start_topic()') | |
71 | + logger.debug(f'[start_topic] topic "{topic}"') | |
72 | 72 | |
73 | 73 | if self.current_topic == topic: |
74 | 74 | logger.info('Restarting current topic is not allowed.') |
... | ... | @@ -76,7 +76,7 @@ class StudentKnowledge(object): |
76 | 76 | |
77 | 77 | # do not allow locked topics |
78 | 78 | if self.is_locked(topic): |
79 | - logger.debug(f'Topic {topic} is locked') | |
79 | + logger.debug(f'[start_topic] topic "{topic}" is locked') | |
80 | 80 | return False |
81 | 81 | |
82 | 82 | # starting new topic |
... | ... | @@ -90,23 +90,20 @@ class StudentKnowledge(object): |
90 | 90 | questions = random.sample(t['questions'], k=k) |
91 | 91 | else: |
92 | 92 | questions = t['questions'][:k] |
93 | - logger.debug(f'Questions: {", ".join(questions)}') | |
93 | + logger.debug(f'[start_topic] questions: {", ".join(questions)}') | |
94 | 94 | |
95 | - # generate instances of questions | |
95 | + # synchronous | |
96 | + # self.questions = [self.factory[ref].generate() | |
97 | + # for ref in questions] | |
96 | 98 | |
97 | - # synchronous: | |
98 | - # self.questions = [self.factory[ref].generate() for ref in questions] | |
99 | - | |
100 | - # async: | |
101 | - loop = asyncio.get_running_loop() | |
102 | - generators = [loop.run_in_executor(None, self.factory[qref].generate) | |
103 | - for qref in questions] | |
104 | - self.questions = await asyncio.gather(*generators) | |
99 | + # asynchronous: | |
100 | + self.questions = [await self.factory[ref].generate_async() | |
101 | + for ref in questions] | |
105 | 102 | |
106 | 103 | # get first question |
107 | 104 | self.next_question() |
108 | 105 | |
109 | - logger.debug(f'Generated {len(self.questions)} questions') | |
106 | + logger.debug(f'[start_topic] generated {len(self.questions)} questions') | |
110 | 107 | return True |
111 | 108 | |
112 | 109 | # ------------------------------------------------------------------------ |
... | ... | @@ -115,7 +112,7 @@ class StudentKnowledge(object): |
115 | 112 | # The current topic is unchanged. |
116 | 113 | # ------------------------------------------------------------------------ |
117 | 114 | def finish_topic(self): |
118 | - logger.debug(f'StudentKnowledge.finish_topic({self.current_topic})') | |
115 | + logger.debug(f'[finish_topic] current_topic {self.current_topic}') | |
119 | 116 | |
120 | 117 | self.state[self.current_topic] = { |
121 | 118 | 'date': datetime.now(), |
... | ... | @@ -132,13 +129,13 @@ class StudentKnowledge(object): |
132 | 129 | # - if wrong, counts number of tries. If exceeded, moves on. |
133 | 130 | # ------------------------------------------------------------------------ |
134 | 131 | async def check_answer(self, answer): |
135 | - logger.debug('StudentKnowledge.check_answer()') | |
132 | + logger.debug('[check_answer]') | |
136 | 133 | |
137 | 134 | q = self.current_question |
138 | 135 | q['answer'] = answer |
139 | 136 | q['finish_time'] = datetime.now() |
140 | 137 | await q.correct_async() |
141 | - logger.debug(f'Grade {q["grade"]:.2} ({q["ref"]})') | |
138 | + logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}') | |
142 | 139 | |
143 | 140 | if q['grade'] > 0.999: |
144 | 141 | self.correct_answers += 1 |
... | ... | @@ -154,7 +151,7 @@ class StudentKnowledge(object): |
154 | 151 | else: |
155 | 152 | action = 'wrong' |
156 | 153 | if self.current_question['append_wrong']: |
157 | - logger.debug("Append new instance of question at the end") | |
154 | + logger.debug('[check_answer] Wrong, append new instance') | |
158 | 155 | self.questions.append(self.factory[q['ref']].generate()) |
159 | 156 | self.next_question() |
160 | 157 | |
... | ... | @@ -175,7 +172,7 @@ class StudentKnowledge(object): |
175 | 172 | default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] |
176 | 173 | maxtries = self.current_question.get('max_tries', default_maxtries) |
177 | 174 | self.current_question['tries'] = maxtries |
178 | - logger.debug(f'Next question is "{self.current_question["ref"]}"') | |
175 | + logger.debug(f'[next_question] "{self.current_question["ref"]}"') | |
179 | 176 | |
180 | 177 | return self.current_question # question or None |
181 | 178 | ... | ... |
aprendizations/learnapp.py
... | ... | @@ -75,7 +75,7 @@ class LearnApp(object): |
75 | 75 | |
76 | 76 | errors = 0 |
77 | 77 | for qref in self.factory: |
78 | - logger.debug(f'Checking "{qref}"...') | |
78 | + logger.debug(f'[sanity_check_questions] Checking "{qref}"...') | |
79 | 79 | try: |
80 | 80 | q = self.factory[qref].generate() |
81 | 81 | except Exception: |
... | ... | @@ -102,7 +102,7 @@ class LearnApp(object): |
102 | 102 | continue # to next test |
103 | 103 | |
104 | 104 | if errors > 0: |
105 | - logger.info(f'{errors:>6} errors found.') | |
105 | + logger.error(f'{errors:>6} errors found.') | |
106 | 106 | raise LearnException('Sanity checks') |
107 | 107 | else: |
108 | 108 | logger.info('No errors found.') |
... | ... | @@ -204,7 +204,7 @@ class LearnApp(object): |
204 | 204 | finishtime=str(q['finish_time']), |
205 | 205 | student_id=uid, |
206 | 206 | topic_id=topic)) |
207 | - logger.debug(f'Saved "{q["ref"]}" into database') | |
207 | + logger.debug(f'[check_answer] Saved "{q["ref"]}" into database') | |
208 | 208 | |
209 | 209 | if knowledge.topic_has_finished(): |
210 | 210 | # finished topic, save into database |
... | ... | @@ -218,7 +218,7 @@ class LearnApp(object): |
218 | 218 | .one_or_none() |
219 | 219 | if a is None: |
220 | 220 | # insert new studenttopic into database |
221 | - logger.debug('Database insert new studenttopic') | |
221 | + logger.debug('[check_answer] Database insert studenttopic') | |
222 | 222 | t = s.query(Topic).get(topic) |
223 | 223 | u = s.query(Student).get(uid) |
224 | 224 | # association object |
... | ... | @@ -227,13 +227,13 @@ class LearnApp(object): |
227 | 227 | u.topics.append(a) |
228 | 228 | else: |
229 | 229 | # update studenttopic in database |
230 | - logger.debug('Database update studenttopic') | |
230 | + logger.debug('[check_answer] Database update studenttopic') | |
231 | 231 | a.level = level |
232 | 232 | a.date = date |
233 | 233 | |
234 | 234 | s.add(a) |
235 | 235 | |
236 | - logger.debug(f'Saved topic "{topic}" into database') | |
236 | + logger.debug(f'[check_answer] Saved topic "{topic}" into database') | |
237 | 237 | |
238 | 238 | return q, action |
239 | 239 | |
... | ... | @@ -244,8 +244,8 @@ class LearnApp(object): |
244 | 244 | student = self.online[uid]['state'] |
245 | 245 | try: |
246 | 246 | await student.start_topic(topic) |
247 | - except Exception: | |
248 | - logger.warning(f'User "{uid}" could not start topic "{topic}"') | |
247 | + except Exception as e: | |
248 | + logger.warning(f'User "{uid}" couldn\'t start "{topic}": {e}') | |
249 | 249 | else: |
250 | 250 | logger.info(f'User "{uid}" started topic "{topic}"') |
251 | 251 | ... | ... |
aprendizations/questions.py
... | ... | @@ -4,12 +4,11 @@ import random |
4 | 4 | import re |
5 | 5 | from os import path |
6 | 6 | import logging |
7 | -import asyncio | |
8 | 7 | from typing import Any, Dict, NewType |
9 | 8 | import uuid |
10 | 9 | |
11 | 10 | # this project |
12 | -from .tools import run_script | |
11 | +from .tools import run_script, run_script_async | |
13 | 12 | |
14 | 13 | # setup logger for this module |
15 | 14 | logger = logging.getLogger(__name__) |
... | ... | @@ -50,8 +49,9 @@ class Question(dict): |
50 | 49 | self['grade'] = 0.0 |
51 | 50 | |
52 | 51 | async def correct_async(self) -> None: |
53 | - loop = asyncio.get_running_loop() | |
54 | - await loop.run_in_executor(None, self.correct) | |
52 | + self.correct() | |
53 | + # loop = asyncio.get_running_loop() | |
54 | + # await loop.run_in_executor(None, self.correct) | |
55 | 55 | |
56 | 56 | def set_defaults(self, d: QDict) -> None: |
57 | 57 | 'Add k:v pairs from default dict d for nonexistent keys' |
... | ... | @@ -305,7 +305,7 @@ class QuestionNumericInterval(Question): |
305 | 305 | answer = float(self['answer'].replace(',', '.', 1)) |
306 | 306 | except ValueError: |
307 | 307 | self['comments'] = ('A resposta tem de ser numérica, ' |
308 | - 'por exemplo 12.345.') | |
308 | + 'por exemplo `12.345`.') | |
309 | 309 | self['grade'] = 0.0 |
310 | 310 | else: |
311 | 311 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 |
... | ... | @@ -318,7 +318,6 @@ class QuestionTextArea(Question): |
318 | 318 | text (str) |
319 | 319 | correct (str with script to run) |
320 | 320 | answer (None or an actual answer) |
321 | - lines (int) | |
322 | 321 | ''' |
323 | 322 | |
324 | 323 | # ------------------------------------------------------------------------ |
... | ... | @@ -327,13 +326,12 @@ class QuestionTextArea(Question): |
327 | 326 | |
328 | 327 | self.set_defaults(QDict({ |
329 | 328 | 'text': '', |
330 | - 'lines': 8, | |
331 | - 'timeout': 5, # seconds | |
329 | + 'timeout': 5, # seconds | |
332 | 330 | 'correct': '', # trying to execute this will fail => grade 0.0 |
333 | 331 | 'args': [] |
334 | 332 | })) |
335 | 333 | |
336 | - self['correct'] = path.join(self['path'], self['correct']) # FIXME | |
334 | + self['correct'] = path.join(self['path'], self['correct']) | |
337 | 335 | |
338 | 336 | # ------------------------------------------------------------------------ |
339 | 337 | def correct(self) -> None: |
... | ... | @@ -347,7 +345,39 @@ class QuestionTextArea(Question): |
347 | 345 | timeout=self['timeout'] |
348 | 346 | ) |
349 | 347 | |
350 | - if isinstance(out, dict): | |
348 | + if out is None: | |
349 | + logger.warning(f'No grade after running "{self["correct"]}".') | |
350 | + self['grade'] = 0.0 | |
351 | + elif isinstance(out, dict): | |
352 | + self['comments'] = out.get('comments', '') | |
353 | + try: | |
354 | + self['grade'] = float(out['grade']) | |
355 | + except ValueError: | |
356 | + logger.error(f'Output error in "{self["correct"]}".') | |
357 | + except KeyError: | |
358 | + logger.error(f'No grade in "{self["correct"]}".') | |
359 | + else: | |
360 | + try: | |
361 | + self['grade'] = float(out) | |
362 | + except (TypeError, ValueError): | |
363 | + logger.error(f'Invalid grade in "{self["correct"]}".') | |
364 | + | |
365 | + # ------------------------------------------------------------------------ | |
366 | + async def correct_async(self) -> None: | |
367 | + super().correct() | |
368 | + | |
369 | + if self['answer'] is not None: # correct answer and parse yaml ouput | |
370 | + out = await run_script_async( | |
371 | + script=self['correct'], | |
372 | + args=self['args'], | |
373 | + stdin=self['answer'], | |
374 | + timeout=self['timeout'] | |
375 | + ) | |
376 | + | |
377 | + if out is None: | |
378 | + logger.warning(f'No grade after running "{self["correct"]}".') | |
379 | + self['grade'] = 0.0 | |
380 | + elif isinstance(out, dict): | |
351 | 381 | self['comments'] = out.get('comments', '') |
352 | 382 | try: |
353 | 383 | self['grade'] = float(out['grade']) |
... | ... | @@ -372,7 +402,6 @@ class QuestionInformation(Question): |
372 | 402 | })) |
373 | 403 | |
374 | 404 | # ------------------------------------------------------------------------ |
375 | - # can return negative values for wrong answers | |
376 | 405 | def correct(self) -> None: |
377 | 406 | super().correct() |
378 | 407 | self['grade'] = 1.0 # always "correct" but points should be zero! |
... | ... | @@ -400,7 +429,7 @@ class QuestionInformation(Question): |
400 | 429 | # # answer one question and correct it |
401 | 430 | # question['answer'] = 42 # set answer |
402 | 431 | # question.correct() # correct answer |
403 | -# print(question['grade']) # print grade | |
432 | +# grade = question['grade'] # get grade | |
404 | 433 | # =========================================================================== |
405 | 434 | class QFactory(object): |
406 | 435 | # Depending on the type of question, a different question class will be |
... | ... | @@ -427,7 +456,7 @@ class QFactory(object): |
427 | 456 | # i.e. a question object (radio, checkbox, ...). |
428 | 457 | # ----------------------------------------------------------------------- |
429 | 458 | def generate(self) -> Question: |
430 | - logger.debug(f'Generating "{self.question["ref"]}"...') | |
459 | + logger.debug(f'[generate] "{self.question["ref"]}"...') | |
431 | 460 | # Shallow copy so that script generated questions will not replace |
432 | 461 | # the original generators |
433 | 462 | q = self.question.copy() |
... | ... | @@ -439,7 +468,7 @@ class QFactory(object): |
439 | 468 | if q['type'] == 'generator': |
440 | 469 | logger.debug(f' \\_ Running "{q["script"]}".') |
441 | 470 | q.setdefault('args', []) |
442 | - q.setdefault('stdin', '') # FIXME does not exist anymore? | |
471 | + q.setdefault('stdin', '') # FIXME is it really necessary? | |
443 | 472 | script = path.join(q['path'], q['script']) |
444 | 473 | out = run_script(script=script, args=q['args'], stdin=q['stdin']) |
445 | 474 | q.update(out) |
... | ... | @@ -455,3 +484,36 @@ class QFactory(object): |
455 | 484 | raise |
456 | 485 | else: |
457 | 486 | return qinstance |
487 | + | |
488 | + # ----------------------------------------------------------------------- | |
489 | + async def generate_async(self) -> Question: | |
490 | + logger.debug(f'[generate_async] "{self.question["ref"]}"...') | |
491 | + # Shallow copy so that script generated questions will not replace | |
492 | + # the original generators | |
493 | + q = self.question.copy() | |
494 | + q['qid'] = str(uuid.uuid4()) # unique for each generated question | |
495 | + | |
496 | + # If question is of generator type, an external program will be run | |
497 | + # which will print a valid question in yaml format to stdout. This | |
498 | + # output is then yaml parsed into a dictionary `q`. | |
499 | + if q['type'] == 'generator': | |
500 | + logger.debug(f' \\_ Running "{q["script"]}".') | |
501 | + q.setdefault('args', []) | |
502 | + q.setdefault('stdin', '') # FIXME is it really necessary? | |
503 | + script = path.join(q['path'], q['script']) | |
504 | + out = await run_script_async(script=script, args=q['args'], | |
505 | + stdin=q['stdin']) | |
506 | + q.update(out) | |
507 | + | |
508 | + # Finally we create an instance of Question() | |
509 | + try: | |
510 | + qinstance = self._types[q['type']](QDict(q)) # of matching class | |
511 | + except QuestionException as e: | |
512 | + logger.error(e) | |
513 | + raise e | |
514 | + except KeyError: | |
515 | + logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') | |
516 | + raise | |
517 | + else: | |
518 | + logger.debug(f'[generate_async] Done instance of {q["ref"]}') | |
519 | + return qinstance | ... | ... |
aprendizations/serve.py
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | |
3 | 3 | # python standard library |
4 | -import sys | |
4 | +import argparse | |
5 | +import asyncio | |
5 | 6 | import base64 |
6 | -import uuid | |
7 | +import functools | |
7 | 8 | import logging.config |
8 | -import argparse | |
9 | 9 | import mimetypes |
10 | +from os import path, environ | |
10 | 11 | import signal |
11 | -import functools | |
12 | 12 | import ssl |
13 | -import asyncio | |
14 | -from os import path, environ | |
13 | +import sys | |
14 | +import uuid | |
15 | + | |
15 | 16 | |
16 | 17 | # third party libraries |
17 | 18 | import tornado.ioloop |
... | ... | @@ -253,7 +254,7 @@ class QuestionHandler(BaseHandler): |
253 | 254 | # --- get question to render |
254 | 255 | @tornado.web.authenticated |
255 | 256 | def get(self): |
256 | - logging.debug('QuestionHandler.get()') | |
257 | + logging.debug('[QuestionHandler.get]') | |
257 | 258 | user = self.current_user |
258 | 259 | q = self.learn.get_current_question(user) |
259 | 260 | |
... | ... | @@ -284,7 +285,7 @@ class QuestionHandler(BaseHandler): |
284 | 285 | # --- post answer, returns what to do next: shake, new_question, finished |
285 | 286 | @tornado.web.authenticated |
286 | 287 | async def post(self) -> None: |
287 | - logging.debug('QuestionHandler.post()') | |
288 | + logging.debug('[QuestionHandler.post]') | |
288 | 289 | user = self.current_user |
289 | 290 | answer = self.get_body_arguments('answer') # list |
290 | 291 | ... | ... |
aprendizations/templates/question-textarea.html
... | ... | @@ -2,7 +2,7 @@ |
2 | 2 | |
3 | 3 | {% block answer %} |
4 | 4 | |
5 | -<textarea class="form-control" rows="{{ question['lines'] }}" name="answer" id="code" autofocus>{{ question['answer'] or '' }}</textarea><br /> | |
5 | +<textarea class="form-control" name="answer" id="code" autofocus>{{ question['answer'] or '' }}</textarea><br /> | |
6 | 6 | <input type="hidden" name="qid" value="{{ question['qid'] }}"> |
7 | 7 | |
8 | 8 | <script> | ... | ... |
aprendizations/tools.py
1 | 1 | |
2 | 2 | # python standard library |
3 | -from os import path | |
4 | -import subprocess | |
3 | +import asyncio | |
5 | 4 | import logging |
5 | +from os import path | |
6 | 6 | import re |
7 | +import subprocess | |
7 | 8 | from typing import Any, List |
8 | 9 | |
9 | 10 | # third party libraries |
10 | -import yaml | |
11 | 11 | import mistune |
12 | 12 | from pygments import highlight |
13 | 13 | from pygments.lexers import get_lexer_by_name |
14 | 14 | from pygments.formatters import HtmlFormatter |
15 | +import yaml | |
16 | + | |
15 | 17 | |
16 | 18 | # setup logger for this module |
17 | 19 | logger = logging.getLogger(__name__) |
... | ... | @@ -101,7 +103,7 @@ class HighlightRenderer(mistune.Renderer): |
101 | 103 | |
102 | 104 | def table(self, header, body): |
103 | 105 | return '<table class="table table-sm"><thead class="thead-light">' \ |
104 | - + header + '</thead><tbody>' + body + "</tbody></table>" | |
106 | + + header + '</thead><tbody>' + body + '</tbody></table>' | |
105 | 107 | |
106 | 108 | def image(self, src, title, alt): |
107 | 109 | alt = mistune.escape(alt, quote=True) |
... | ... | @@ -168,7 +170,7 @@ def load_yaml(filename: str, default: Any = None) -> Any: |
168 | 170 | def run_script(script: str, |
169 | 171 | args: List[str] = [], |
170 | 172 | stdin: str = '', |
171 | - timeout: int = 5) -> Any: | |
173 | + timeout: int = 2) -> Any: | |
172 | 174 | |
173 | 175 | script = path.expanduser(script) |
174 | 176 | try: |
... | ... | @@ -200,3 +202,41 @@ def run_script(script: str, |
200 | 202 | logger.error(f'Error parsing yaml output of "{script}"') |
201 | 203 | else: |
202 | 204 | return output |
205 | + | |
206 | + | |
207 | +# ---------------------------------------------------------------------------- | |
208 | +# Same as above, but asynchronous | |
209 | +# ---------------------------------------------------------------------------- | |
210 | +async def run_script_async(script: str, | |
211 | + args: List[str] = [], | |
212 | + stdin: str = '', | |
213 | + timeout: int = 2) -> Any: | |
214 | + | |
215 | + script = path.expanduser(script) | |
216 | + args = [str(a) for a in args] | |
217 | + | |
218 | + p = await asyncio.create_subprocess_exec( | |
219 | + script, *args, | |
220 | + stdin=asyncio.subprocess.PIPE, | |
221 | + stdout=asyncio.subprocess.PIPE, | |
222 | + stderr=asyncio.subprocess.DEVNULL, | |
223 | + ) | |
224 | + | |
225 | + try: | |
226 | + stdout, stderr = await asyncio.wait_for( | |
227 | + p.communicate(input=stdin.encode('utf-8')), | |
228 | + timeout=timeout | |
229 | + ) | |
230 | + except asyncio.TimeoutError: | |
231 | + logger.warning(f'Timeout {timeout}s running script "{script}".') | |
232 | + return | |
233 | + | |
234 | + if p.returncode != 0: | |
235 | + logger.error(f'Return code {p.returncode} running "{script}".') | |
236 | + else: | |
237 | + try: | |
238 | + output = yaml.safe_load(stdout.decode('utf-8', 'ignore')) | |
239 | + except Exception: | |
240 | + logger.error(f'Error parsing yaml output of "{script}"') | |
241 | + else: | |
242 | + return output | ... | ... |
demo/math/questions.yaml
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | - $2\times(9-2\times 3)$ |
17 | 17 | correct: [1, 1, 0, 0, 0] |
18 | 18 | choose: 3 |
19 | + max_tries: 1 | |
19 | 20 | solution: | |
20 | 21 | Colocando o 2 em evidência, obtém-se a resposta correcta $2\times(9 - 3)$ |
21 | 22 | ou $(9-3)\times 2$. |
... | ... | @@ -50,6 +51,7 @@ |
50 | 51 | - 12 |
51 | 52 | - 14 |
52 | 53 | - 1, a **unidade** |
54 | + max_tries: 1 | |
53 | 55 | solution: | |
54 | 56 | O único número primo é o 13. |
55 | 57 | |
... | ... | @@ -76,9 +78,10 @@ |
76 | 78 | números negativos são menores que os positivos. |
77 | 79 | |
78 | 80 | # --------------------------------------------------------------------------- |
81 | +# the program should print a question in yaml format. The args will be | |
82 | +# sent as command line options when the program is run. | |
79 | 83 | - type: generator |
80 | 84 | ref: overflow |
81 | 85 | script: gen-multiples-of-3.py |
82 | 86 | args: [11, 120] |
83 | - # the program should print a question in yaml format. The args will be | |
84 | - # sent as command line options when the program is run. | |
87 | + choose: 3 | ... | ... |
demo/solar_system/correct-first_3_planets.py
... | ... | @@ -2,26 +2,27 @@ |
2 | 2 | |
3 | 3 | import re |
4 | 4 | import sys |
5 | +import time | |
5 | 6 | |
6 | 7 | s = sys.stdin.read() |
7 | 8 | |
8 | -# set of words converted to lowercase | |
9 | -answer = set(re.findall(r'[\w]+', s.lower())) | |
10 | -answer.difference_update({'e', 'a', 'planeta', 'planetas'}) # ignore these | |
9 | +ans = set(re.findall(r'[\w]+', s.lower())) # convert answer to lowercase | |
10 | +ans.difference_update({'e', 'a', 'o', 'planeta', 'planetas'}) # ignore words | |
11 | 11 | |
12 | 12 | # correct set of planets |
13 | 13 | planets = {'mercúrio', 'vénus', 'terra'} |
14 | 14 | |
15 | -correct = set.intersection(answer, planets) # the ones I got right | |
16 | -wrong = set.difference(answer, planets) # the ones I got wrong | |
15 | +correct = set.intersection(ans, planets) # the ones I got right | |
16 | +wrong = set.difference(ans, planets) # the ones I got wrong | |
17 | 17 | |
18 | 18 | grade = (len(correct) - len(wrong)) / len(planets) |
19 | 19 | |
20 | +comments = 'Certo' if grade == 1.0 else 'as iniciais dos planetas são M, V e T' | |
21 | + | |
20 | 22 | out = f'''--- |
21 | -grade: {grade}''' | |
23 | +grade: {grade} | |
24 | +comments: {comments}''' | |
22 | 25 | |
23 | -if grade < 1.0: | |
24 | - out += '\ncomments: Vou dar uma ajuda, as iniciais são M, V e T...' | |
26 | +time.sleep(2) # simulate computation time (may generate timeout) | |
25 | 27 | |
26 | 28 | print(out) |
27 | -exit(0) | ... | ... |
demo/solar_system/questions.yaml