Compare View
Commits (4)
Showing
29 changed files
Show diff stats
.gitignore
BUGS.md
... | ... | @@ -2,6 +2,15 @@ |
2 | 2 | |
3 | 3 | ## BUGS |
4 | 4 | |
5 | +- usar [xsrf_cookie_name](https://www.tornadoweb.org/en/stable/releases/v6.3.0.html) | |
6 | +- usar [asyncio.loop.add_signal_handler](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.add_signal_handler) | |
7 | + em vez de `signal.signal`. | |
8 | +- fim do topico "servidor nao responde": /Users/mjsb/Repos/aprendizations/aprendizations/learnapp.py:296: SAWarning: Multiple rows returned with uselist=False for lazily-loaded attribute 'Topic.students' (This warning originated from the Session 'autoflush' process, which was invoked automatically in response to a user-initiated operation.) | |
9 | + student.topics.append(student_topic) | |
10 | + /Users/mjsb/Repos/aprendizations/aprendizations/learnapp.py:296: SAWarning: Object of type <StudentTopic> not in session, add operation along 'Topic.students' will not proceed (This warning originated from the Session 'autoflush' process, which was invoked automatically in response to a user-initiated operation.) | |
11 | + student.topics.append(student_topic) | |
12 | +- rendering das tabelas errado, e.g. computer/logic/operators:equivalence | |
13 | +- warning ao arrancar: could not start course "favicon.ico" | |
5 | 14 | - nao esta a respeitar o numero de tentativas `max_tries`. |
6 | 15 | - se na especificacao de um curso, a referencia do topico nao existir como |
7 | 16 | directorio, rebenta. |
... | ... | @@ -23,6 +32,8 @@ |
23 | 32 | |
24 | 33 | ## TODO |
25 | 34 | |
35 | +- RGPD: possibilidade ter alunos anonimos (sem numero e nome), poderem | |
36 | +unregister. | |
26 | 37 | - ordenação que faça sentido, organizada por chapters. |
27 | 38 | - chapters deviam ser automaticamente checkados... |
28 | 39 | - hints dos topicos fechados com as dependencias desse topico que ainda faltam | ... | ... |
aprendizations/__init__.py
1 | -# Copyright © 2022 Miguel Barão | |
1 | +# Copyright © 2025 Miguel Barão | |
2 | 2 | # |
3 | 3 | # THE MIT License |
4 | 4 | # |
... | ... | @@ -30,10 +30,10 @@ are progressively uncovered as the students progress. |
30 | 30 | ''' |
31 | 31 | |
32 | 32 | APP_NAME = 'aprendizations' |
33 | -APP_VERSION = '2023.2.dev1' | |
33 | +APP_VERSION = '2025.1.dev1' | |
34 | 34 | APP_DESCRIPTION = __doc__ |
35 | 35 | |
36 | 36 | __author__ = 'Miguel Barão' |
37 | -__copyright__ = 'Copyright © 2023, Miguel Barão' | |
37 | +__copyright__ = 'Copyright © 2025, Miguel Barão' | |
38 | 38 | __license__ = 'MIT license' |
39 | 39 | __version__ = APP_VERSION | ... | ... |
aprendizations/learnapp.py
... | ... | @@ -110,7 +110,7 @@ class Application(): |
110 | 110 | def _sanity_check_questions(self) -> None: |
111 | 111 | ''' |
112 | 112 | Unit tests for all questions |
113 | - | |
113 | + | |
114 | 114 | Generates all questions, give right and wrong answers and corrects. |
115 | 115 | ''' |
116 | 116 | |
... | ... | @@ -274,7 +274,7 @@ class Application(): |
274 | 274 | tid: str = student_state.get_previous_topic() |
275 | 275 | level: float = student_state.get_topic_level(tid) |
276 | 276 | date: str = str(student_state.get_topic_date(tid)) |
277 | - logger.info('"%s" finished "%s" (level=%.2f)', uid, tid, level) | |
277 | + logger.info('User "%s" finished topic "%s" (level=%.2f)', uid, tid, level) | |
278 | 278 | |
279 | 279 | with Session(self._engine) as session: |
280 | 280 | query = select(StudentTopic) \ | ... | ... |
aprendizations/main.py
... | ... | @@ -168,9 +168,9 @@ def main(): |
168 | 168 | logger.info('LearnApp started') |
169 | 169 | |
170 | 170 | # --- run webserver forever ---------------------------------------------- |
171 | - asyncio.run(webserver(app=app, | |
172 | - ssl=ssl_ctx, | |
173 | - port=arg.port, | |
171 | + asyncio.run(webserver(app=app, | |
172 | + ssl=ssl_ctx, | |
173 | + port=arg.port, | |
174 | 174 | debug=arg.debug)) |
175 | 175 | logger.critical('Webserver stopped.') |
176 | 176 | ... | ... |
aprendizations/renderer_markdown.py
... | ... | @@ -10,6 +10,9 @@ from pygments.formatters import html |
10 | 10 | |
11 | 11 | class Renderer(mistune.HTMLRenderer): |
12 | 12 | def block_code(self, code, info=None): |
13 | + ''' | |
14 | + Code syntax highlight using pygments | |
15 | + ''' | |
13 | 16 | if info is not None: |
14 | 17 | lexer = get_lexer_by_name(info, stripall=True) |
15 | 18 | formatter = html.HtmlFormatter() |
... | ... | @@ -17,6 +20,9 @@ class Renderer(mistune.HTMLRenderer): |
17 | 20 | return f'<pre><code>{mistune.escape(code)}</code></pre>' |
18 | 21 | |
19 | 22 | def image(self, text, url, title=None): |
23 | + ''' | |
24 | + Include image title and alternative text | |
25 | + ''' | |
20 | 26 | text = mistune.escape(text, quote=True) |
21 | 27 | title = mistune.escape(title or '', quote=True) |
22 | 28 | return (f'<img src="/file/{url}" alt="{text}" title="{title}"' | ... | ... |
aprendizations/serve.py
... | ... | @@ -6,18 +6,16 @@ Tornado Webserver |
6 | 6 | # python standard library |
7 | 7 | import asyncio |
8 | 8 | import base64 |
9 | -from logging import getLogger | |
10 | 9 | import mimetypes |
11 | -from os.path import join, dirname, expanduser | |
12 | 10 | import signal |
13 | 11 | import sys |
14 | -from typing import List, Optional, Union | |
15 | 12 | import uuid |
13 | +import logging | |
14 | +from os.path import join, dirname, expanduser | |
15 | +from typing import List, Optional, Union | |
16 | 16 | |
17 | 17 | # third party libraries |
18 | -import tornado.httpserver | |
19 | -import tornado.ioloop | |
20 | -import tornado.web | |
18 | +import tornado | |
21 | 19 | from tornado.escape import to_unicode |
22 | 20 | |
23 | 21 | # this project |
... | ... | @@ -26,7 +24,7 @@ from aprendizations.learnapp import LearnException |
26 | 24 | |
27 | 25 | |
28 | 26 | # setup logger for this module |
29 | -logger = getLogger(__name__) | |
27 | +logger = logging.getLogger(__name__) | |
30 | 28 | |
31 | 29 | |
32 | 30 | # ============================================================================ |
... | ... | @@ -40,7 +38,7 @@ class BaseHandler(tornado.web.RequestHandler): |
40 | 38 | |
41 | 39 | def get_current_user(self): |
42 | 40 | '''called on every method decorated with @tornado.web.authenticated''' |
43 | - cookie = self.get_secure_cookie('aprendizations_user') | |
41 | + cookie = self.get_signed_cookie('aprendizations_user') | |
44 | 42 | return None if cookie is None else to_unicode(cookie) |
45 | 43 | |
46 | 44 | |
... | ... | @@ -59,7 +57,7 @@ class LoginHandler(BaseHandler): |
59 | 57 | loop = tornado.ioloop.IOLoop.current() |
60 | 58 | login_ok = await self.app.login(uid, pw, loop) |
61 | 59 | if login_ok: |
62 | - self.set_secure_cookie('aprendizations_user', uid) | |
60 | + self.set_signed_cookie('aprendizations_user', uid) | |
63 | 61 | self.redirect('/') |
64 | 62 | else: |
65 | 63 | self.render('login.html', error='Número ou senha incorrectos') |
... | ... | @@ -391,7 +389,7 @@ def signal_handler(*_) -> None: |
391 | 389 | ''' |
392 | 390 | reply = input(' --> Stop webserver? (yes/no) ') |
393 | 391 | if reply.lower() == 'yes': |
394 | - tornado.ioloop.IOLoop.current().stop() | |
392 | + tornado.ioloop.IOLoop.current().stop() # FIXME: is there a recent alternative? | |
395 | 393 | logger.critical('Webserver stopped.') |
396 | 394 | sys.exit(0) |
397 | 395 | |
... | ... | @@ -403,23 +401,25 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: |
403 | 401 | ''' |
404 | 402 | |
405 | 403 | # --- create web application |
404 | + opts = { 'app': app } | |
406 | 405 | handlers = [ |
407 | - (r'/login', LoginHandler, dict(app=app)), | |
408 | - (r'/logout', LogoutHandler, dict(app=app)), | |
409 | - (r'/change_password', ChangePasswordHandler, dict(app=app)), | |
410 | - (r'/question', QuestionHandler, dict(app=app)), # render question | |
411 | - (r'/rankings', RankingsHandler, dict(app=app)), # rankings table | |
412 | - (r'/topic/(.+)', TopicHandler, dict(app=app)), # start topic | |
413 | - (r'/file/(.+)', FileHandler, dict(app=app)), # serve file | |
414 | - (r'/courses', CoursesHandler, dict(app=app)), # show available courses | |
415 | - (r'/course/(.*)', CourseHandler, dict(app=app)), # show topics from course | |
416 | - (r'/course2/(.*)', CourseHandler2, dict(app=app)), # show topics from course FIXME | |
417 | - (r'/', RootHandler, dict(app=app)), # redirects | |
406 | + (r'/login', LoginHandler, opts), | |
407 | + (r'/logout', LogoutHandler, opts), | |
408 | + (r'/change_password', ChangePasswordHandler, opts), | |
409 | + (r'/question', QuestionHandler, opts), # render question | |
410 | + (r'/rankings', RankingsHandler, opts), # rankings table | |
411 | + (r'/topic/(.+)', TopicHandler, opts), # start topic | |
412 | + (r'/file/(.+)', FileHandler, opts), # serve file | |
413 | + (r'/courses', CoursesHandler, opts), # show available courses | |
414 | + (r'/course/(.*)', CourseHandler, opts), # show topics from course | |
415 | + (r'/course2/(.*)', CourseHandler2, opts), # show topics from course FIXME: | |
416 | + (r'/', RootHandler, opts), # redirects | |
418 | 417 | ] |
419 | 418 | settings = { |
420 | 419 | 'template_path': join(dirname(__file__), 'templates'), |
421 | 420 | 'static_path': join(dirname(__file__), 'static'), |
422 | 421 | 'static_url_prefix': '/static/', |
422 | + # TODO: xsrf_cookie_name | |
423 | 423 | 'xsrf_cookies': True, |
424 | 424 | 'cookie_secret': base64.b64encode(uuid.uuid4().bytes), |
425 | 425 | 'login_url': '/login', | ... | ... |
aprendizations/student.py
... | ... | @@ -50,35 +50,34 @@ class StudentState(): |
50 | 50 | self.factory = factory # question factory |
51 | 51 | self.courses = courses # {'course': ['topic_id1', 'topic_id2',...]} |
52 | 52 | |
53 | - # data of this student | |
53 | + # student data | |
54 | 54 | self.uid = uid # user id '12345' |
55 | 55 | self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} |
56 | 56 | |
57 | 57 | # prepare for running |
58 | 58 | self.update_topic_levels() # applies forgetting factor |
59 | 59 | self.unlock_topics() # whose dependencies have been completed |
60 | - self.start_course(None) | |
60 | + | |
61 | + logger.debug('no active course') | |
62 | + self.current_course: Optional[str] = None | |
63 | + self.topic_sequence: List[str] = [] | |
64 | + self.current_topic: Optional[str] = None | |
65 | + self.previous_topic: Optional[str] = None | |
61 | 66 | |
62 | 67 | # ------------------------------------------------------------------------ |
63 | - def start_course(self, course: Optional[str]) -> None: | |
68 | + def start_course(self, course: str) -> None: | |
64 | 69 | ''' |
65 | 70 | Tries to start a course. |
66 | 71 | Finds the recommended sequence of topics for the student. |
67 | 72 | ''' |
68 | - if course is None: | |
69 | - logger.debug('no active course') | |
70 | - self.current_course: Optional[str] = None | |
71 | - self.topic_sequence: List[str] = [] | |
72 | - self.current_topic: Optional[str] = None | |
73 | - else: | |
74 | - try: | |
75 | - topics = self.courses[course]['goals'] | |
76 | - except KeyError: | |
77 | - logger.debug('course "%s" does not exist', course) | |
78 | - raise | |
79 | - logger.debug('starting course "%s"', course) | |
80 | - self.current_course = course | |
81 | - self.topic_sequence = self._recommend_sequence(topics) | |
73 | + try: | |
74 | + topics = self.courses[course]['goals'] | |
75 | + except KeyError: | |
76 | + logger.debug('course "%s" does not exist', course) | |
77 | + raise | |
78 | + logger.debug('starting course "%s"', course) | |
79 | + self.current_course = course | |
80 | + self.topic_sequence = self._recommend_sequence(topics) | |
82 | 81 | |
83 | 82 | # ------------------------------------------------------------------------ |
84 | 83 | async def start_topic(self, topic_ref: str) -> None: | ... | ... |
aprendizations/templates/include-head.html
1 | 1 | <meta charset="utf-8" /> |
2 | 2 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
3 | 3 | <meta name="author" content="Miguel Barão"> |
4 | -<link rel="icon" href="favicon.ico"> | |
4 | +<!-- <link href="{{static_url('favicon.ico')}}" rel="icon" /> --> | |
5 | 5 | <title>aprendizations</title> | ... | ... |
aprendizations/templates/include-libs.html
1 | 1 | <!-- jquery --> |
2 | -<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> | |
2 | +<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> | |
3 | +<!-- <script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script> --> | |
4 | +<!-- <script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> --> | |
3 | 5 | |
4 | 6 | <!-- bootstrap --> |
5 | -<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> | |
6 | -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> | |
7 | +<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"> | |
8 | +<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script> | |
9 | +<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous"> --> | |
10 | +<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script> --> | |
11 | +<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> --> | |
12 | +<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> --> | |
7 | 13 | |
8 | 14 | <!-- bootstrap icons --> |
9 | -<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"> | |
15 | +<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css"> | |
16 | +<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"> --> | |
17 | +<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"> --> | |
10 | 18 | |
11 | 19 | <!-- MathJax --> |
12 | 20 | <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> |
13 | 21 | <script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> |
14 | 22 | |
15 | 23 | <!-- codemirror --> |
16 | -<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | |
17 | -<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.js" integrity="sha512-rdFIN28+neM8H8zNsjRClhJb1fIYby2YCNmoqwnqBDEvZgpcp7MJiX8Wd+Oi6KcJOMOuvGztjrsI59rly9BsVQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> | |
24 | +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | |
25 | +<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js" integrity="sha512-sSWQXoxIkE0G4/xqLngx5C53oOZCgFRxWE79CvMX2X0IKx14W3j9Dpz/2MpRh58xb2W/h+Y4WAHJQA0qMMuxJg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> | |
26 | +<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> --> | |
27 | +<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.js" integrity="sha512-rdFIN28+neM8H8zNsjRClhJb1fIYby2YCNmoqwnqBDEvZgpcp7MJiX8Wd+Oi6KcJOMOuvGztjrsI59rly9BsVQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> --> | |
18 | 28 | |
19 | 29 | <!-- animate --> |
20 | 30 | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" integrity="sha512-c42qTSw/wPZ3/5LBzD+Bw5f7bSF2oxou6wEb+I/lqeaKV5FDIfMvvRp772y4jcJLKuGUOpbJMdg/BTl50fJYAw==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | ... | ... |
aprendizations/templates/topic.html
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 | {% include include-libs.html %} |
6 | 6 | |
7 | 7 | <!-- local --> |
8 | + <link rel="icon" href="{{static_url('favicon.ico')}}" /> | |
8 | 9 | <link rel="stylesheet" href="{{static_url('css/github.css')}}" /> |
9 | 10 | <link rel="stylesheet" href="{{static_url('css/topic.css')}}" /> |
10 | 11 | <script defer src="{{static_url('js/topic.js')}}"></script> | ... | ... |
aprendizations/tools.py
1 | 1 | |
2 | 2 | # python standard library |
3 | -import asyncio | |
3 | +# import asyncio | |
4 | 4 | import logging |
5 | 5 | from os import path |
6 | 6 | # import re |
7 | 7 | # import subprocess |
8 | -from typing import Any, List | |
8 | +from typing import Any | |
9 | 9 | |
10 | 10 | # third party libraries |
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 | 17 | # setup logger for this module |
19 | 18 | logger = logging.getLogger(__name__) |
20 | 19 | |
21 | 20 | |
22 | -# ------------------------------------------------------------------------- | |
23 | -# Block math: | |
24 | -# $$x$$ or \begin{equation}x\end{equation} | |
25 | -# ------------------------------------------------------------------------- | |
21 | +# # ------------------------------------------------------------------------- | |
22 | +# # Block math: | |
23 | +# # $$x$$ or \begin{equation}x\end{equation} | |
24 | +# # ------------------------------------------------------------------------- | |
26 | 25 | # class MathBlockGrammar(mistune.BlockGrammar): |
27 | 26 | # block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL) |
28 | 27 | # latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}', |
... | ... | @@ -53,7 +52,6 @@ logger = logging.getLogger(__name__) |
53 | 52 | # 'text': m.group(2) |
54 | 53 | # }) |
55 | 54 | # |
56 | -# | |
57 | 55 | # # ------------------------------------------------------------------------- |
58 | 56 | # # Inline math: $x$ |
59 | 57 | # # ------------------------------------------------------------------------- |
... | ... | @@ -94,6 +92,7 @@ logger = logging.getLogger(__name__) |
94 | 92 | # self.token['text']) |
95 | 93 | # |
96 | 94 | # |
95 | +# | |
97 | 96 | # class HighlightRenderer(mistune.Renderer): |
98 | 97 | # def block_code(self, code, lang='text'): |
99 | 98 | # try: |
... | ... | @@ -136,118 +135,32 @@ logger = logging.getLogger(__name__) |
136 | 135 | # else: |
137 | 136 | # return md |
138 | 137 | # |
138 | +# | |
139 | 139 | |
140 | -# --------------------------------------------------------------------------- | |
141 | -# load data from yaml file | |
142 | -# --------------------------------------------------------------------------- | |
143 | -def load_yaml(filename: str, default: Any = None) -> Any: | |
144 | - filename = path.expanduser(filename) | |
145 | - try: | |
146 | - f = open(filename, 'r', encoding='utf-8') | |
147 | - except Exception as e: | |
148 | - logger.error(e) | |
149 | - if default is not None: | |
150 | - return default | |
151 | - else: | |
152 | - raise | |
153 | - | |
154 | - with f: | |
155 | - try: | |
156 | - return yaml.safe_load(f) | |
157 | - except yaml.YAMLError as e: | |
158 | - logger.error(str(e).replace('\n', ' ')) | |
159 | - if default is not None: | |
160 | - return default | |
161 | - else: | |
162 | - raise | |
163 | 140 | |
164 | 141 | |
165 | 142 | # --------------------------------------------------------------------------- |
166 | -# Runs a script and returns its stdout parsed as yaml, or None on error. | |
167 | -# The script is run in another process but this function blocks waiting | |
168 | -# for its termination. | |
169 | -# --------------------------------------------------------------------------- | |
170 | -# def run_script(script: str, | |
171 | -# args: List[str] = [], | |
172 | -# stdin: str = '', | |
173 | -# timeout: int = 2) -> Any: | |
174 | -# | |
175 | -# script = path.expanduser(script) | |
143 | +# TODO: unused, to be removed | |
144 | +# def load_yaml(filename: str, default: Any = None) -> Any: | |
145 | +# ''' | |
146 | +# Load data from yaml file | |
147 | +# ''' | |
148 | +# filename = path.expanduser(filename) | |
176 | 149 | # try: |
177 | -# cmd = [script] + [str(a) for a in args] | |
178 | -# p = subprocess.run(cmd, | |
179 | -# input=stdin, | |
180 | -# stdout=subprocess.PIPE, | |
181 | -# stderr=subprocess.STDOUT, | |
182 | -# text=True, # same as universal_newlines=True | |
183 | -# timeout=timeout, | |
184 | -# ) | |
185 | -# except FileNotFoundError: | |
186 | -# logger.error(f'Can not execute script "{script}": not found.') | |
187 | -# except PermissionError: | |
188 | -# logger.error(f'Can not execute script "{script}": wrong permissions.') | |
189 | -# except OSError: | |
190 | -# logger.error(f'Can not execute script "{script}": unknown reason.') | |
191 | -# except subprocess.TimeoutExpired: | |
192 | -# logger.error(f'Timeout {timeout}s exceeded while running "{script}".') | |
193 | -# except Exception: | |
194 | -# logger.error(f'An Exception ocurred running {script}.') | |
195 | -# else: | |
196 | -# if p.returncode != 0: | |
197 | -# logger.error(f'Return code {p.returncode} running "{script}".') | |
150 | +# f = open(filename, 'r', encoding='utf-8') | |
151 | +# except Exception as e: | |
152 | +# logger.error(e) | |
153 | +# if default is not None: | |
154 | +# return default | |
198 | 155 | # else: |
199 | -# try: | |
200 | -# output = yaml.safe_load(p.stdout) | |
201 | -# except Exception: | |
202 | -# logger.error(f'Error parsing yaml output of "{script}"') | |
203 | -# else: | |
204 | -# return output | |
205 | -# | |
206 | - | |
207 | -# def run_script(script: str, | |
208 | -# args: List[str] = [], | |
209 | -# stdin: str = '', | |
210 | -# timeout: int = 2) -> Any: | |
211 | -# asyncio.run(run_script_async(script, args, stdin, timeout)) | |
212 | -# ---------------------------------------------------------------------------- | |
213 | -# Same as above, but asynchronous | |
214 | -# ---------------------------------------------------------------------------- | |
215 | -# async def run_script(script: str, | |
216 | -# args: List[str] = [], | |
217 | -# stdin: str = '', | |
218 | -# timeout: int = 5) -> Any: | |
219 | -# | |
220 | -# # normalize args | |
221 | -# script = path.expanduser(script) | |
222 | -# input_bytes = stdin.encode('utf-8') | |
223 | -# args = [str(a) for a in args] | |
156 | +# raise | |
224 | 157 | # |
225 | -# try: | |
226 | -# p = await asyncio.create_subprocess_exec( | |
227 | -# script, *args, | |
228 | -# stdin=asyncio.subprocess.PIPE, | |
229 | -# stdout=asyncio.subprocess.PIPE, | |
230 | -# stderr=asyncio.subprocess.DEVNULL, | |
231 | -# ) | |
232 | -# except FileNotFoundError: | |
233 | -# logger.error(f'Can not execute script "{script}": not found.') | |
234 | -# except PermissionError: | |
235 | -# logger.error(f'Can not execute script "{script}": wrong permissions.') | |
236 | -# except OSError: | |
237 | -# logger.error(f'Can not execute script "{script}": unknown reason.') | |
238 | -# else: | |
158 | +# with f: | |
239 | 159 | # try: |
240 | -# stdout, _ = await asyncio.wait_for(p.communicate(input_bytes), timeout) | |
241 | -# except asyncio.TimeoutError: | |
242 | -# logger.warning(f'Timeout {timeout}s exceeded running "{script}".') | |
243 | -# return | |
244 | -# | |
245 | -# if p.returncode != 0: | |
246 | -# logger.error(f'Return code {p.returncode} running "{script}".') | |
247 | -# else: | |
248 | -# try: | |
249 | -# output = yaml.safe_load(stdout.decode('utf-8', 'ignore')) | |
250 | -# except Exception: | |
251 | -# logger.error(f'Error parsing yaml output of "{script}"') | |
160 | +# return yaml.safe_load(f) | |
161 | +# except yaml.YAMLError as e: | |
162 | +# logger.error(str(e).replace('\n', ' ')) | |
163 | +# if default is not None: | |
164 | +# return default | |
252 | 165 | # else: |
253 | -# return output | |
166 | +# raise | ... | ... |
demo/astronomy.yaml
... | ... | @@ -17,8 +17,14 @@ |
17 | 17 | # ---------------------------------------------------------------------------- |
18 | 18 | |
19 | 19 | topics: |
20 | - astronomy/solar-system: | |
21 | - name: Sistema solar | |
20 | + astronomy/solar-system/planets: | |
21 | + name: Planets | |
22 | + | |
23 | + astronomy/solar-system/natural_satellites: | |
24 | + name: Natural satellites | |
25 | + deps: | |
26 | + - astronomy/solar-system/planets | |
27 | + | |
22 | 28 | |
23 | 29 | # astronomy/milky-way: |
24 | 30 | # name: Via Láctea | ... | ... |
demo/astronomy/solar-system/correct-first_3_planets.py
... | ... | @@ -1,28 +0,0 @@ |
1 | -#!/usr/bin/env python3 | |
2 | - | |
3 | -import re | |
4 | -import sys | |
5 | -import time | |
6 | - | |
7 | -s = sys.stdin.read() | |
8 | - | |
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 | - | |
12 | -# correct set of planets | |
13 | -planets = {'mercúrio', 'vénus', 'terra'} | |
14 | - | |
15 | -correct = set.intersection(ans, planets) # the ones I got right | |
16 | -wrong = set.difference(ans, planets) # the ones I got wrong | |
17 | - | |
18 | -grade = (len(correct) - len(wrong)) / len(planets) | |
19 | - | |
20 | -comments = 'Certo' if grade == 1.0 else 'as iniciais dos planetas são M, V e T' | |
21 | - | |
22 | -out = f'''--- | |
23 | -grade: {grade} | |
24 | -comments: {comments}''' | |
25 | - | |
26 | -time.sleep(2) # simulate computation time (may generate timeout) | |
27 | - | |
28 | -print(out) |
demo/astronomy/solar-system/public/earth.jpg
1.02 MB
demo/astronomy/solar-system/public/jupiter.gif
2.96 MB
demo/astronomy/solar-system/public/planets.png
419 KB
demo/astronomy/solar-system/public/saturn.jpg
117 KB
demo/astronomy/solar-system/questions.yaml
... | ... | @@ -1,67 +0,0 @@ |
1 | ---- | |
2 | -# ---------------------------------------------------------------------------- | |
3 | -- type: text | |
4 | - ref: planet-earth | |
5 | - title: Sistema solar | |
6 | - text: O nosso planeta chama-se planeta... | |
7 | - correct: ['Terra', 'terra'] | |
8 | - # opcional | |
9 | - answer: dos macacos? | |
10 | - solution: | | |
11 | - O nosso planeta é o planeta **Terra**. | |
12 | - | |
13 | -  | |
14 | - | |
15 | -# ---------------------------------------------------------------------------- | |
16 | -- type: radio | |
17 | - ref: largest-planet | |
18 | - title: Sistema solar | |
19 | - text: | | |
20 | -  | |
21 | - | |
22 | - Qual é o maior planeta do Sistema Solar? | |
23 | - options: | |
24 | - - Mercúrio | |
25 | - - Marte | |
26 | - - Júpiter | |
27 | - - Saturno | |
28 | - - Têm todos o mesmo tamanho | |
29 | - # opcional | |
30 | - correct: 2 | |
31 | - # discount: true | |
32 | - shuffle: false | |
33 | - solution: | | |
34 | - O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas | |
35 | - ainda assim 2.5 vezes maior que a massa de todos os outros planetas juntos. | |
36 | - É um gigante gasoso maioritariamente composto por hidrogénio. | |
37 | - | |
38 | -  | |
39 | - | |
40 | -# ---------------------------------------------------------------------------- | |
41 | -- type: text-regex | |
42 | - ref: saturn | |
43 | - title: Sistema solar | |
44 | - text: O planeta do sistema solar conhecido pelos seus aneis é o planeta... | |
45 | - correct: '[Ss]aturno' | |
46 | - solution: | | |
47 | - O planeta Saturno é famoso pelos seus anéis. | |
48 | - É o segundo maior planeta do Sistema Solar. | |
49 | - Tal como Júpiter, é um gigante gasoso. | |
50 | - Os seus anéis são formados por partículas de gelo. | |
51 | - | |
52 | -  | |
53 | - | |
54 | -# ---------------------------------------------------------------------------- | |
55 | -- type: textarea | |
56 | - ref: first_3_planets | |
57 | - title: Sistema solar | |
58 | - text: | | |
59 | - Qual o nome dos três planetas mais próximos do Sol? | |
60 | - Exemplo `Ceres, Krypton e Vulcano` | |
61 | - correct: correct-first_3_planets.py | |
62 | - # correct: correct-timeout.py | |
63 | - # opcional | |
64 | - answer: Ceres, Krypton e Vulcano | |
65 | - timeout: 3 | |
66 | - solution: | | |
67 | - Os 3 planetas mais perto do Sol são Mercúrio, Vénus e Terra. |
demo/courses.yaml
... | ... | @@ -11,19 +11,20 @@ topics_from: |
11 | 11 | # ---------------------------------------------------------------------------- |
12 | 12 | courses: |
13 | 13 | math: |
14 | - title: Matemática | |
14 | + title: Elementary math | |
15 | 15 | description: | |
16 | - Adição, multiplicação e números primos. | |
16 | + Arithmetic operations: add, subtract, mulitply and divide numbers | |
17 | 17 | goals: |
18 | - - math | |
19 | 18 | - math/addition |
19 | + - math/subtraction | |
20 | 20 | - math/multiplication |
21 | - - math/prime-numbers | |
21 | + - math/division | |
22 | 22 | |
23 | 23 | astronomy: |
24 | - title: Astronomia | |
24 | + title: Astronomy | |
25 | 25 | description: | |
26 | - Sistema Solar e Via Láctea. | |
26 | + This course is about the planets and natural satellites in our Solar | |
27 | + System. | |
27 | 28 | goals: |
28 | - - astronomy/solar-system | |
29 | - # - astronomy/milky-way | |
29 | + - astronomy/solar-system/planets | |
30 | + - astronomy/solar-system/natural_satellites | ... | ... |
demo/math.yaml
... | ... | @@ -18,30 +18,20 @@ |
18 | 18 | |
19 | 19 | topics: |
20 | 20 | math/addition: |
21 | - name: Adição | |
21 | + name: Addition | |
22 | 22 | |
23 | - math/multiplication: | |
24 | - name: Multiplicação | |
23 | + math/subtraction: | |
24 | + name: Subtraction | |
25 | 25 | deps: |
26 | 26 | - math/addition |
27 | 27 | |
28 | - math/learn-prime-numbers: | |
29 | - name: O que são números primos? | |
30 | - type: learn | |
31 | - file: learn.yaml | |
32 | - deps: | |
33 | - - math/multiplication | |
34 | - | |
35 | - math/prime-numbers: | |
36 | - name: Números primos | |
28 | + math/multiplication: | |
29 | + name: Multiplication | |
37 | 30 | deps: |
38 | - - math/learn-prime-numbers | |
31 | + - math/addition | |
39 | 32 | |
40 | - math: | |
41 | - name: Números e operações aritméticas | |
42 | - type: chapter | |
33 | + math/division: | |
34 | + name: Division | |
43 | 35 | deps: |
44 | - - math/addition | |
36 | + - math/subtraction | |
45 | 37 | - math/multiplication |
46 | - - math/learn-prime-numbers | |
47 | - - math/prime-numbers | ... | ... |
... | ... | @@ -0,0 +1,34 @@ |
1 | +#!/usr/bin/env python3 | |
2 | + | |
3 | +''' | |
4 | +This question generator expects two integer arguments. | |
5 | +These are the range for the number of coins in each pocket. | |
6 | +''' | |
7 | + | |
8 | +import random | |
9 | +import sys | |
10 | + | |
11 | +a = int(sys.argv[1]) | |
12 | +b = int(sys.argv[2]) | |
13 | + | |
14 | +x, y = random.sample(range(a, b), k=2) | |
15 | +r = x + y | |
16 | + | |
17 | +pocket1, pocket2 = random.sample(['left pocket', 'right pocket', 'wallet', | |
18 | + 'safe at home'], k=2) | |
19 | + | |
20 | +currency = random.choice(['Euros', 'US dollars', 'British pounds']) | |
21 | + | |
22 | +print(f'''--- | |
23 | +type: text | |
24 | +title: Adding two numbers | |
25 | +text: | | |
26 | + Suppose you have {x} {currency} in your {pocket1} and {y} in your | |
27 | + {pocket2}. | |
28 | + How many {currency} do you have? | |
29 | + | |
30 | + Just answer the number, for example `42`. | |
31 | +transform: ['trim'] | |
32 | +correct: ['{r}'] | |
33 | +solution: | | |
34 | + You have a total of {r} {currency}.''') | ... | ... |
demo/math/addition/questions.yaml
1 | 1 | --- |
2 | 2 | # --------------------------------------------------------------------------- |
3 | 3 | - type: generator |
4 | - ref: addition-two-digits | |
5 | - script: addition-two-digits.py | |
6 | - args: [10, 20] | |
7 | - | |
8 | -# --------------------------------------------------------------------------- | |
9 | -- type: checkbox | |
10 | - ref: addition-properties | |
11 | - title: Propriedades da adição | |
12 | - text: Indique quais as propriedades que a adição satisfaz. | |
13 | - options: | |
14 | - # right | |
15 | - - Existência de elemento neutro, $x+0=x$. | |
16 | - - Existência de inverso aditivo (simétrico), $x+(-x)=0$. | |
17 | - - Propriedade associativa, $(x+y)+z = x+(y+z)$. | |
18 | - - Propriedade comutativa, $x+y=y+x$. | |
19 | - # wrong | |
20 | - - Existência de elemento absorvente, $x+1=1$. | |
21 | - correct: [1, 1, 1, 1, 0] | |
22 | - solution: | | |
23 | - A adição não tem elemento absorvente. | |
4 | + ref: addition | |
5 | + script: addition.py | |
6 | + args: [2, 20] | ... | ... |
demo/math/multiplication/multiplication-table.py
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | |
3 | +import json | |
3 | 4 | import random |
4 | 5 | |
5 | 6 | x, y = random.sample(range(2,10), k=2) |
... | ... | @@ -8,15 +9,38 @@ r = x * y |
8 | 9 | yy = '+'.join([str(y)]*x) |
9 | 10 | xx = '+'.join([str(x)]*y) |
10 | 11 | |
11 | -print(f'''--- | |
12 | -type: text | |
13 | -title: Multiplicação (tabuada) | |
14 | -text: | | |
15 | - Qual o resultado da multiplicação ${x}\\times {y}$? | |
16 | -transform: ['trim'] | |
17 | -correct: ['{r}'] | |
18 | -solution: | | |
19 | - A multiplicação é a repetição da soma. Podemos fazer de duas maneiras: | |
20 | - $$ {x}\\times {y} = {yy} = {r} $$ | |
21 | - ou | |
22 | - $$ {y}\\times {x} = {xx} = {r}. $$''') | |
12 | +xbullets = x * '*' # ⏺ | |
13 | +xxbullets = '\n'.join(y * [xbullets]) | |
14 | + | |
15 | +ybullets = y * '*' | |
16 | +yybullets = '\n'.join(x * [ybullets]) | |
17 | + | |
18 | +solution = f''' | |
19 | +Multiplying is essentially repeating the addition multiple times. | |
20 | +We can do it in two ways: | |
21 | + | |
22 | +$$ | |
23 | +{x}\\times {y} = {yy} = {r} | |
24 | +$$ | |
25 | +or | |
26 | +$$ | |
27 | +{y}\\times {x} = {xx} = {r}. | |
28 | +$$ | |
29 | + | |
30 | +Multiplication can also be seen as the number of dots organised in rectangular | |
31 | +shape: | |
32 | + | |
33 | +{xxbullets} | |
34 | + | |
35 | +{yybullets} | |
36 | +''' | |
37 | + | |
38 | +question = { | |
39 | + 'type': 'text', | |
40 | + 'title': 'Multiplication (table)', | |
41 | + 'text': f'What is the result of ${x}\\times {y}$?', | |
42 | + 'transform': ['trim'], | |
43 | + 'correct': [f'{r}'], | |
44 | + 'solution': solution, | |
45 | +} | |
46 | +print(json.dumps(question)) | ... | ... |
demo/math/multiplication/questions.yaml
1 | 1 | --- |
2 | 2 | # ---------------------------------------------------------------------------- |
3 | -- type: generator | |
4 | - ref: multiplication-table | |
5 | - script: multiplication-table.py | |
3 | +# - type: generator | |
4 | +# ref: multiplication-table | |
5 | +# script: multiplication-table.py | |
6 | 6 | |
7 | 7 | # ---------------------------------------------------------------------------- |
8 | 8 | - type: checkbox |
9 | - ref: multiplication-properties | |
10 | - title: Propriedades da multiplicação | |
11 | - text: Indique quais as propriedades que a multiplicação satisfaz. | |
9 | + ref: multiplication | |
10 | + title: Multiplication | |
11 | + text: | | |
12 | + Which of the following multiplications have the result $12$? | |
12 | 13 | options: |
13 | - # right | |
14 | - - Existência de elemento neutro, $1x=x$. | |
15 | - - Propriedade associativa, $(xy)z = x(yz)$. | |
16 | - - Propriedade comutativa, $xy=yx$. | |
17 | - - Existência de elemento absorvente, $0x=0$. | |
18 | - # wrong | |
19 | - - Existência de inverso, todos os números $x$ tem um inverso $1/x$ tal que | |
20 | - $x(1/x)=1$. | |
21 | - correct: [1, 1, 1, 1, 0] | |
14 | + - $6\times 2$ | |
15 | + - $2\times 6$ | |
16 | + - $3\times 4$ | |
17 | + - $12\times 0$ | |
18 | + correct: [1, 1, 1, 0] | |
22 | 19 | solution: | |
23 | - Na multiplicação nem todos os números têm inverso. Só têm inverso os números | |
24 | - diferentes de zero. | |
25 | - As outras propriedades são satisfeitas para todos os números. | |
20 | + Multiplying by zero is always zero. | |
21 | + The others are correct. | |
22 | + | |
23 | + | |
24 | +# # ---------------------------------------------------------------------------- | |
25 | +# - type: checkbox | |
26 | +# ref: multiplication-properties | |
27 | +# title: Multiplication Properties | |
28 | +# text: Which properties does the multiplication have? | |
29 | +# options: | |
30 | +# # right | |
31 | +# - 'Existence of an identity element: $1 x = x$.' | |
32 | +# - 'Associative property: $(x y) z = x (y z)$.' | |
33 | +# - 'Commutative property: $x y = y x$.' | |
34 | +# - 'Existence of an absorbing element: $0 x = 0$.' | |
35 | +# # wrong | |
36 | +# - 'Inverse property: all numbers have an inverse $1/x$ such that | |
37 | +# $x(1/x)=1$.' | |
38 | +# correct: [1, 1, 1, 1, 0] | |
39 | +# solution: | | |
40 | +# Not all numbers have an inverse: *zero* does not have an inverse. | |
41 | +# The other properties are true. | ... | ... |
demo/math/questions.yaml
mypy.ini
1 | -; [mypy] | |
2 | -; python_version = 3.10 | |
1 | +[mypy] | |
2 | +python_version = 3.11 | |
3 | 3 | ; plugins = sqlalchemy.ext.mypy.plugin |
4 | 4 | |
5 | -; [mypy-pygments.*] | |
6 | -; ignore_missing_imports = True | |
5 | +[mypy-networkx.*] | |
6 | +ignore_missing_imports = True | |
7 | 7 | |
8 | -; [mypy-networkx.*] | |
9 | -; ignore_missing_imports = True | |
10 | - | |
11 | -; [mypy-bcrypt.*] | |
12 | -; ignore_missing_imports = True | |
13 | - | |
14 | -; [mypy-mistune.*] | |
15 | -; ignore_missing_imports = True | |
8 | +[mypy-mistune.*] | |
9 | +ignore_missing_imports = True | ... | ... |
setup.py
... | ... | @@ -7,36 +7,36 @@ with open("README.md", "r") as f: |
7 | 7 | long_description = f.read() |
8 | 8 | |
9 | 9 | setup( |
10 | - name=APP_NAME, | |
11 | - version=APP_VERSION, | |
12 | - author=__author__, | |
13 | - author_email="mjsb@uevora.pt", | |
14 | - license=__license__, | |
15 | - description=APP_DESCRIPTION.split('\n')[0], | |
16 | - long_description=APP_DESCRIPTION, | |
17 | - long_description_content_type="text/markdown", | |
18 | - url="https://git.xdi.uevora.pt/mjsb/aprendizations.git", | |
19 | - packages=find_packages(), | |
20 | - include_package_data=True, # install files from MANIFEST.in | |
21 | - python_requires='>=3.9', | |
22 | - install_requires=[ | |
23 | - 'tornado>=6.2', | |
24 | - 'mistune>=3.0.0rc4', | |
10 | + name = APP_NAME, | |
11 | + version = APP_VERSION, | |
12 | + author = __author__, | |
13 | + author_email = "mjsb@uevora.pt", | |
14 | + license = __license__, | |
15 | + description = APP_DESCRIPTION.split('\n')[0], | |
16 | + long_description = APP_DESCRIPTION, | |
17 | + long_description_content_type = "text/markdown", | |
18 | + url = "https://git.xdi.uevora.pt/mjsb/aprendizations.git", | |
19 | + packages = find_packages(), | |
20 | + include_package_data = True, # install files from MANIFEST.in | |
21 | + python_requires = '>=3.11.1', | |
22 | + install_requires = [ | |
23 | + 'tornado>=6.4', | |
24 | + 'mistune>=3.1', | |
25 | 25 | 'pyyaml>=6.0', |
26 | - 'pygments>=2.14', | |
27 | - 'sqlalchemy>=2.0.0', | |
28 | - 'bcrypt>=4.0.1', | |
29 | - 'networkx>=3.0', | |
26 | + 'pygments>=2.19', | |
27 | + 'sqlalchemy>=2.0.37', | |
28 | + 'bcrypt>=4.2.1', | |
29 | + 'networkx>=3.4.2' | |
30 | 30 | 'pandas>=2.3', |
31 | 31 | 'openpyxl' |
32 | 32 | ], |
33 | - entry_points={ | |
33 | + entry_points = { | |
34 | 34 | 'console_scripts': [ |
35 | 35 | 'aprendizations = aprendizations.main:main', |
36 | 36 | 'initdb-aprendizations = aprendizations.initdb:main', |
37 | 37 | ] |
38 | 38 | }, |
39 | - classifiers=[ | |
39 | + classifiers = [ | |
40 | 40 | 'Development Status :: 4 - Beta', |
41 | 41 | 'Environment :: Console', |
42 | 42 | 'Intended Audience :: Education', | ... | ... |