Compare View
Commits (4)
-
move from bcrypt to argon2 ignore .venv directory update sqlalchemy models to 2.0
Showing
9 changed files
Show diff stats
.gitignore
@@ -0,0 +1,17 @@ | @@ -0,0 +1,17 @@ | ||
1 | +# TODO | ||
2 | + | ||
3 | +- opcao correct de testes que nao foram corrigidos automaticamente. | ||
4 | +- tests should store identification of the perguntations version. | ||
5 | +- detailed CSV devia ter as refs das perguntas e so preencher as que o aluno respondeu. | ||
6 | +- long pooling admin | ||
7 | +- student sends updated answers | ||
8 | +- questions with parts | ||
9 | + | ||
10 | + | ||
11 | +---- Estados ----- | ||
12 | + | ||
13 | +inicialização (sincrona) | ||
14 | + test factory | ||
15 | + database setup: define lista de estudantes com teste=None | ||
16 | + login admin: | ||
17 | + gera todos os testes para os alunos na lista |
perguntations/__init__.py
@@ -21,7 +21,7 @@ | @@ -21,7 +21,7 @@ | ||
21 | # SOFTWARE. | 21 | # SOFTWARE. |
22 | 22 | ||
23 | 23 | ||
24 | -"""A webserver for running tests in class | 24 | +'''A webserver for running tests in class |
25 | 25 | ||
26 | perguntations is a webserver that is intended to be run in an isolated | 26 | perguntations is a webserver that is intended to be run in an isolated |
27 | local network in classroom environment. | 27 | local network in classroom environment. |
@@ -29,13 +29,13 @@ The server will run a preconfigured test and serve the test as a web form. | @@ -29,13 +29,13 @@ The server will run a preconfigured test and serve the test as a web form. | ||
29 | The answers submitted by the students are immediatly checked and classified. | 29 | The answers submitted by the students are immediatly checked and classified. |
30 | The grades are stored in a database and the tests are stored in JSON files as | 30 | The grades are stored in a database and the tests are stored in JSON files as |
31 | proof of submission and for review. | 31 | proof of submission and for review. |
32 | -""" | 32 | +''' |
33 | 33 | ||
34 | -APP_NAME = "perguntations" | ||
35 | -APP_VERSION = "2022.04.dev1" | 34 | +APP_NAME = 'perguntations' |
35 | +APP_VERSION = '2024.07.dev1' | ||
36 | APP_DESCRIPTION = str(__doc__) | 36 | APP_DESCRIPTION = str(__doc__) |
37 | 37 | ||
38 | -__author__ = "Miguel Barão" | ||
39 | -__copyright__ = "Copyright 2022, Miguel Barão" | ||
40 | -__license__ = "MIT license" | 38 | +__author__ = 'Miguel Barão' |
39 | +__copyright__ = 'Copyright 2024, Miguel Barão' | ||
40 | +__license__ = 'MIT license' | ||
41 | __version__ = APP_VERSION | 41 | __version__ = APP_VERSION |
perguntations/app.py
@@ -14,7 +14,7 @@ import os | @@ -14,7 +14,7 @@ import os | ||
14 | from typing import Optional | 14 | from typing import Optional |
15 | 15 | ||
16 | # installed packages | 16 | # installed packages |
17 | -import bcrypt | 17 | +import argon2 |
18 | from sqlalchemy import create_engine, select | 18 | from sqlalchemy import create_engine, select |
19 | from sqlalchemy.exc import OperationalError, NoResultFound, IntegrityError | 19 | from sqlalchemy.exc import OperationalError, NoResultFound, IntegrityError |
20 | from sqlalchemy.orm import Session | 20 | from sqlalchemy.orm import Session |
@@ -30,21 +30,24 @@ from .questions import question_from | @@ -30,21 +30,24 @@ from .questions import question_from | ||
30 | # setup logger for this module | 30 | # setup logger for this module |
31 | logger = logging.getLogger(__name__) | 31 | logger = logging.getLogger(__name__) |
32 | 32 | ||
33 | +# ============================================================================ | ||
34 | +ph = argon2.PasswordHasher() | ||
33 | 35 | ||
34 | -async def check_password(password: str, hashed: bytes) -> bool: | 36 | +async def check_password(password: str, hashed: str) -> bool: |
35 | """check password in executor""" | 37 | """check password in executor""" |
36 | loop = asyncio.get_running_loop() | 38 | loop = asyncio.get_running_loop() |
37 | - return await loop.run_in_executor( | ||
38 | - None, bcrypt.checkpw, password.encode("utf-8"), hashed | ||
39 | - ) | ||
40 | - | ||
41 | - | ||
42 | -async def hash_password(password: str) -> bytes: | 39 | + try: |
40 | + return await loop.run_in_executor(None, ph.verify, hashed, password) | ||
41 | + except argon2.exceptions.VerifyMismatchError: | ||
42 | + return False | ||
43 | + except Exception: | ||
44 | + logger.error('Unkown error while verifying password') | ||
45 | + return False | ||
46 | + | ||
47 | +async def hash_password(password: str) -> str: | ||
43 | """get hash for password""" | 48 | """get hash for password""" |
44 | loop = asyncio.get_running_loop() | 49 | loop = asyncio.get_running_loop() |
45 | - return await loop.run_in_executor( | ||
46 | - None, bcrypt.hashpw, password.encode("utf-8"), bcrypt.gensalt() | ||
47 | - ) | 50 | + return await loop.run_in_executor(None, ph.hash, password) |
48 | 51 | ||
49 | 52 | ||
50 | # ============================================================================ | 53 | # ============================================================================ |
@@ -66,7 +69,7 @@ class App: | @@ -66,7 +69,7 @@ class App: | ||
66 | self._make_test_factory(config["testfile"]) | 69 | self._make_test_factory(config["testfile"]) |
67 | self._db_setup() # setup engine and load all students | 70 | self._db_setup() # setup engine and load all students |
68 | 71 | ||
69 | - # FIXME get_event_loop will be deprecated in python3.10 | 72 | + # FIXME: get_event_loop will be deprecated in python3. |
70 | asyncio.get_event_loop().run_until_complete(self._assign_tests()) | 73 | asyncio.get_event_loop().run_until_complete(self._assign_tests()) |
71 | 74 | ||
72 | # command line options: --allow-all, --allow-list filename | 75 | # command line options: --allow-all, --allow-list filename |
perguntations/initdb.py
@@ -9,10 +9,9 @@ import csv | @@ -9,10 +9,9 @@ import csv | ||
9 | import argparse | 9 | import argparse |
10 | import re | 10 | import re |
11 | from string import capwords | 11 | from string import capwords |
12 | -from concurrent.futures import ThreadPoolExecutor | ||
13 | 12 | ||
14 | # installed packages | 13 | # installed packages |
15 | -import bcrypt | 14 | +from argon2 import PasswordHasher |
16 | from sqlalchemy import create_engine, select | 15 | from sqlalchemy import create_engine, select |
17 | from sqlalchemy.orm import Session | 16 | from sqlalchemy.orm import Session |
18 | from sqlalchemy.exc import IntegrityError | 17 | from sqlalchemy.exc import IntegrityError |
@@ -80,7 +79,7 @@ def parse_commandline_arguments(): | @@ -80,7 +79,7 @@ def parse_commandline_arguments(): | ||
80 | 79 | ||
81 | 80 | ||
82 | # ============================================================================ | 81 | # ============================================================================ |
83 | -def get_students_from_csv(filename): | 82 | +def get_students_from_csv(filename: str): |
84 | """ | 83 | """ |
85 | SIIUE names have alien strings like "(TE)" and are sometimes capitalized | 84 | SIIUE names have alien strings like "(TE)" and are sometimes capitalized |
86 | We remove them so that students dont keep asking what it means | 85 | We remove them so that students dont keep asking what it means |
@@ -111,17 +110,6 @@ def get_students_from_csv(filename): | @@ -111,17 +110,6 @@ def get_students_from_csv(filename): | ||
111 | return students | 110 | return students |
112 | 111 | ||
113 | 112 | ||
114 | -# ============================================================================ | ||
115 | -def hashpw(student, password=None) -> None: | ||
116 | - """replace password by hash for a single student""" | ||
117 | - print(".", end="", flush=True) | ||
118 | - if password is None: | ||
119 | - student["pw"] = "" | ||
120 | - else: | ||
121 | - student["pw"] = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) | ||
122 | - | ||
123 | - | ||
124 | -# ============================================================================ | ||
125 | def insert_students_into_db(session, students) -> None: | 113 | def insert_students_into_db(session, students) -> None: |
126 | """insert list of students into the database""" | 114 | """insert list of students into the database""" |
127 | try: | 115 | try: |
@@ -129,7 +117,6 @@ def insert_students_into_db(session, students) -> None: | @@ -129,7 +117,6 @@ def insert_students_into_db(session, students) -> None: | ||
129 | [Student(id=s["uid"], name=s["name"], password=s["pw"]) for s in students] | 117 | [Student(id=s["uid"], name=s["name"], password=s["pw"]) for s in students] |
130 | ) | 118 | ) |
131 | session.commit() | 119 | session.commit() |
132 | - | ||
133 | except IntegrityError: | 120 | except IntegrityError: |
134 | print("!!! Integrity error. Users already in database. Aborted !!!\n") | 121 | print("!!! Integrity error. Users already in database. Aborted !!!\n") |
135 | session.rollback() | 122 | session.rollback() |
@@ -139,7 +126,6 @@ def insert_students_into_db(session, students) -> None: | @@ -139,7 +126,6 @@ def insert_students_into_db(session, students) -> None: | ||
139 | def show_students_in_database(session, verbose=False): | 126 | def show_students_in_database(session, verbose=False): |
140 | """get students from database""" | 127 | """get students from database""" |
141 | users = session.execute(select(Student)).scalars().all() | 128 | users = session.execute(select(Student)).scalars().all() |
142 | - # users = session.query(Student).all() | ||
143 | total = len(users) | 129 | total = len(users) |
144 | 130 | ||
145 | print("Registered users:") | 131 | print("Registered users:") |
@@ -165,15 +151,16 @@ def show_students_in_database(session, verbose=False): | @@ -165,15 +151,16 @@ def show_students_in_database(session, verbose=False): | ||
165 | def main(): | 151 | def main(): |
166 | """insert, update, show students from database""" | 152 | """insert, update, show students from database""" |
167 | 153 | ||
154 | + ph = PasswordHasher() | ||
168 | args = parse_commandline_arguments() | 155 | args = parse_commandline_arguments() |
169 | 156 | ||
170 | # --- database | 157 | # --- database |
171 | print(f"Database: {args.db}") | 158 | print(f"Database: {args.db}") |
172 | engine = create_engine(f"sqlite:///{args.db}", echo=False, future=True) | 159 | engine = create_engine(f"sqlite:///{args.db}", echo=False, future=True) |
173 | Base.metadata.create_all(engine) # Criates schema if needed | 160 | Base.metadata.create_all(engine) # Criates schema if needed |
174 | - session = Session(engine, future=True) | 161 | + session = Session(engine, future=True) # FIXME: future? |
175 | 162 | ||
176 | - # --- make list of students to insert | 163 | + # --- build list of new students to insert |
177 | students = [] | 164 | students = [] |
178 | 165 | ||
179 | if args.admin: | 166 | if args.admin: |
@@ -191,35 +178,45 @@ def main(): | @@ -191,35 +178,45 @@ def main(): | ||
191 | 178 | ||
192 | # --- insert new students | 179 | # --- insert new students |
193 | if students: | 180 | if students: |
194 | - print("Generating password hashes", end="") | ||
195 | - with ThreadPoolExecutor() as executor: # hashing | ||
196 | - executor.map(lambda s: hashpw(s, args.pw), students) | ||
197 | - print(f"\nInserting {len(students)}") | 181 | + if args.pw is None: |
182 | + print('Set passwords to empty') | ||
183 | + for s in students: | ||
184 | + s['pw'] = '' | ||
185 | + else: | ||
186 | + print('Generating password hashes') | ||
187 | + for s in students: | ||
188 | + s['pw'] = ph.hash(args.pw) | ||
189 | + print('.', end='', flush=True) | ||
190 | + print(f'\nInserting {len(students)}') | ||
198 | insert_students_into_db(session, students) | 191 | insert_students_into_db(session, students) |
199 | 192 | ||
200 | # --- update all students | 193 | # --- update all students |
201 | if args.update_all: | 194 | if args.update_all: |
202 | - all_students = ( | ||
203 | - session.execute(select(Student).where(Student.id != "0")).scalars().all() | ||
204 | - ) | ||
205 | - | ||
206 | - print(f"Updating password of {len(all_students)} users", end="") | ||
207 | - for student in all_students: | ||
208 | - password = (args.pw or student.id).encode("utf-8") | ||
209 | - student.password = bcrypt.hashpw(password, bcrypt.gensalt()) | ||
210 | - print(".", end="", flush=True) | ||
211 | - print() | 195 | + query = select(Student).where(Student.id != '0') |
196 | + all_students = session.execute(query).scalars().all() | ||
197 | + if args.pw is None: | ||
198 | + print(f'Resetting password of {len(all_students)} users') | ||
199 | + for student in all_students: | ||
200 | + student.password = '' | ||
201 | + else: | ||
202 | + print(f'Updating password of {len(all_students)} users') | ||
203 | + for student in all_students: | ||
204 | + student.password = ph.hash(args.pw) | ||
205 | + print('.', end='', flush=True) | ||
206 | + print() | ||
212 | session.commit() | 207 | session.commit() |
213 | 208 | ||
214 | - # --- update some students | 209 | + # --- update only specified students |
215 | else: | 210 | else: |
216 | for student_id in args.update: | 211 | for student_id in args.update: |
217 | - print(f"Updating password of {student_id}") | ||
218 | - student = session.execute( | ||
219 | - select(Student).where(Student.id == student_id) | ||
220 | - ).scalar_one() | ||
221 | - new_password = (args.pw or student_id).encode("utf-8") | ||
222 | - student.password = bcrypt.hashpw(new_password, bcrypt.gensalt()) | 212 | + query = select(Student).where(Student.id == student_id) |
213 | + student = session.execute(query).scalar_one() | ||
214 | + if args.pw is None: | ||
215 | + print(f'Resetting password of user {student_id}') | ||
216 | + student.password = '' | ||
217 | + else: | ||
218 | + print(f'Updating password of user {student_id}') | ||
219 | + student.password = ph.hash(args.pw) | ||
223 | session.commit() | 220 | session.commit() |
224 | 221 | ||
225 | show_students_in_database(session, args.verbose) | 222 | show_students_in_database(session, args.verbose) |
perguntations/models.py
@@ -3,100 +3,59 @@ perguntations/models.py | @@ -3,100 +3,59 @@ perguntations/models.py | ||
3 | SQLAlchemy ORM | 3 | SQLAlchemy ORM |
4 | """ | 4 | """ |
5 | 5 | ||
6 | -from typing import Any | 6 | +from typing import List |
7 | 7 | ||
8 | -from sqlalchemy import Column, ForeignKey, Integer, Float, String | ||
9 | -from sqlalchemy.orm import declarative_base, relationship | 8 | +from sqlalchemy import ForeignKey, Integer, String |
9 | +from sqlalchemy.orm import DeclarativeBase, Mapped, relationship, mapped_column | ||
10 | 10 | ||
11 | 11 | ||
12 | -# FIXME Any is a workaround for static type checking | ||
13 | -# (https://github.com/python/mypy/issues/6372) | ||
14 | -Base: Any = declarative_base() | 12 | +class Base(DeclarativeBase): |
13 | + pass | ||
15 | 14 | ||
16 | 15 | ||
16 | +# [Student] ---1:N--- [Test] ---1:N--- [Question] | ||
17 | + | ||
17 | # ---------------------------------------------------------------------------- | 18 | # ---------------------------------------------------------------------------- |
18 | class Student(Base): | 19 | class Student(Base): |
19 | - """Student table""" | ||
20 | - | ||
21 | - __tablename__ = "students" | ||
22 | - id = Column(String, primary_key=True) | ||
23 | - name = Column(String) | ||
24 | - password = Column(String) | 20 | + __tablename__ = 'students' |
25 | 21 | ||
22 | + id = mapped_column(String, primary_key=True) | ||
23 | + name: Mapped[str] | ||
24 | + password: Mapped[str] | ||
26 | # --- | 25 | # --- |
27 | - tests = relationship("Test", back_populates="student") | ||
28 | - | ||
29 | - def __repr__(self): | ||
30 | - return ( | ||
31 | - f"Student(" | ||
32 | - f"id={self.id!r}, " | ||
33 | - f"name={self.name!r}, " | ||
34 | - f"password={self.password!r})" | ||
35 | - ) | ||
36 | - | 26 | + tests: Mapped[List['Test']] = relationship(back_populates='student') |
37 | 27 | ||
38 | # ---------------------------------------------------------------------------- | 28 | # ---------------------------------------------------------------------------- |
39 | class Test(Base): | 29 | class Test(Base): |
40 | - """Test table""" | ||
41 | - | ||
42 | - __tablename__ = "tests" | ||
43 | - id = Column(Integer, primary_key=True) # auto_increment | ||
44 | - ref = Column(String) | ||
45 | - title = Column(String) | ||
46 | - grade = Column(Float) | ||
47 | - state = Column(String) # ACTIVE, SUBMITTED, CORRECTED, QUIT, NULL | ||
48 | - comment = Column(String) | ||
49 | - starttime = Column(String) | ||
50 | - finishtime = Column(String) | ||
51 | - filename = Column(String) | ||
52 | - student_id = Column(String, ForeignKey("students.id")) | ||
53 | - | 30 | + __tablename__ = 'tests' |
31 | + | ||
32 | + id = mapped_column(Integer, primary_key=True) # auto_increment | ||
33 | + ref: Mapped[str] | ||
34 | + title: Mapped[str] | ||
35 | + grade: Mapped[float] | ||
36 | + state: Mapped[str] # ACTIVE, SUBMITTED, CORRECTED, QUIT, NULL | ||
37 | + comment: Mapped[str] | ||
38 | + starttime: Mapped[str] | ||
39 | + finishtime: Mapped[str] | ||
40 | + filename: Mapped[str] | ||
41 | + student_id = mapped_column(String, ForeignKey('students.id')) | ||
54 | # --- | 42 | # --- |
55 | - student = relationship("Student", back_populates="tests") | ||
56 | - questions = relationship("Question", back_populates="test") | ||
57 | - | ||
58 | - def __repr__(self): | ||
59 | - return ( | ||
60 | - f"Test(" | ||
61 | - f"id={self.id!r}, " | ||
62 | - f"ref={self.ref!r}, " | ||
63 | - f"title={self.title!r}, " | ||
64 | - f"grade={self.grade!r}, " | ||
65 | - f"state={self.state!r}, " | ||
66 | - f"comment={self.comment!r}, " | ||
67 | - f"starttime={self.starttime!r}, " | ||
68 | - f"finishtime={self.finishtime!r}, " | ||
69 | - f"filename={self.filename!r}, " | ||
70 | - f"student_id={self.student_id!r})" | ||
71 | - ) | ||
72 | 43 | ||
44 | + student: Mapped['Student'] = relationship(back_populates='tests') | ||
45 | + questions: Mapped[List['Question']] = relationship(back_populates='test') | ||
73 | 46 | ||
74 | # --------------------------------------------------------------------------- | 47 | # --------------------------------------------------------------------------- |
75 | class Question(Base): | 48 | class Question(Base): |
76 | - """Question table""" | ||
77 | - | ||
78 | - __tablename__ = "questions" | ||
79 | - id = Column(Integer, primary_key=True) # auto_increment | ||
80 | - number = Column(Integer) # question number (ref may be not be unique) | ||
81 | - ref = Column(String) | ||
82 | - grade = Column(Float) | ||
83 | - comment = Column(String) | ||
84 | - starttime = Column(String) | ||
85 | - finishtime = Column(String) | ||
86 | - test_id = Column(String, ForeignKey("tests.id")) | ||
87 | - | 49 | + __tablename__ = 'questions' |
50 | + | ||
51 | + id = mapped_column(Integer, primary_key=True) # auto_increment | ||
52 | + number: Mapped[int] # question number (ref may be repeated in the same test) | ||
53 | + ref: Mapped[str] | ||
54 | + grade: Mapped[float] | ||
55 | + comment: Mapped[str] | ||
56 | + starttime: Mapped[str] | ||
57 | + finishtime: Mapped[str] | ||
58 | + test_id = mapped_column(String, ForeignKey('tests.id')) | ||
88 | # --- | 59 | # --- |
89 | - test = relationship("Test", back_populates="questions") | ||
90 | 60 | ||
91 | - def __repr__(self): | ||
92 | - return ( | ||
93 | - f"Question(" | ||
94 | - f"id={self.id!r}, " | ||
95 | - f"number={self.number!r}, " | ||
96 | - f"ref={self.ref!r}, " | ||
97 | - f"grade={self.grade!r}, " | ||
98 | - f"comment={self.comment!r}, " | ||
99 | - f"starttime={self.starttime!r}, " | ||
100 | - f"finishtime={self.finishtime!r}, " | ||
101 | - f"test_id={self.test_id!r})" | ||
102 | - ) | 61 | + test: Mapped['Test'] = relationship(back_populates='questions') |
perguntations/serve.py
1 | -#!/usr/bin/env python3 | ||
2 | 1 | ||
3 | """ | 2 | """ |
4 | Handles the web, http & html part of the application interface. | 3 | Handles the web, http & html part of the application interface. |
@@ -21,9 +20,7 @@ from typing import Dict, Tuple | @@ -21,9 +20,7 @@ from typing import Dict, Tuple | ||
21 | import uuid | 20 | import uuid |
22 | 21 | ||
23 | # user installed libraries | 22 | # user installed libraries |
24 | -import tornado.ioloop | ||
25 | -import tornado.web | ||
26 | -import tornado.httpserver | 23 | +import tornado |
27 | 24 | ||
28 | # this project | 25 | # this project |
29 | from .parser_markdown import md_to_html | 26 | from .parser_markdown import md_to_html |
@@ -262,7 +259,7 @@ class RootHandler(BaseHandler): | @@ -262,7 +259,7 @@ class RootHandler(BaseHandler): | ||
262 | 259 | ||
263 | # ---------------------------------------------------------------------------- | 260 | # ---------------------------------------------------------------------------- |
264 | # pylint: disable=abstract-method | 261 | # pylint: disable=abstract-method |
265 | -# FIXME also to update answers | 262 | +# FIXME: also to update answers |
266 | class StudentWebservice(BaseHandler): | 263 | class StudentWebservice(BaseHandler): |
267 | """ | 264 | """ |
268 | Receive ajax from students during the test in response to the events | 265 | Receive ajax from students during the test in response to the events |
perguntations/testfactory.py
@@ -66,7 +66,7 @@ test_schema = schema.Schema( | @@ -66,7 +66,7 @@ test_schema = schema.Schema( | ||
66 | }, | 66 | }, |
67 | ignore_extra_keys=True, | 67 | ignore_extra_keys=True, |
68 | ) | 68 | ) |
69 | -# FIXME schema error with 'testfile' which is added in the code | 69 | +# FIXME: schema error with 'testfile' which is added in the code |
70 | 70 | ||
71 | # ============================================================================ | 71 | # ============================================================================ |
72 | class TestFactoryException(Exception): | 72 | class TestFactoryException(Exception): |
@@ -105,7 +105,7 @@ class TestFactory(dict): | @@ -105,7 +105,7 @@ class TestFactory(dict): | ||
105 | normalize_question_list(self["questions"]) | 105 | normalize_question_list(self["questions"]) |
106 | 106 | ||
107 | # --- for review, we are done. no factories needed | 107 | # --- for review, we are done. no factories needed |
108 | - # if self['review']: FIXME | 108 | + # if self['review']: FIXME: |
109 | # logger.info('Review mode. No questions loaded. No factories.') | 109 | # logger.info('Review mode. No questions loaded. No factories.') |
110 | # return | 110 | # return |
111 | 111 | ||
@@ -267,7 +267,7 @@ class TestFactory(dict): | @@ -267,7 +267,7 @@ class TestFactory(dict): | ||
267 | checks if questions can be correctly generated and corrected | 267 | checks if questions can be correctly generated and corrected |
268 | """ | 268 | """ |
269 | logger.info("Checking questions...") | 269 | logger.info("Checking questions...") |
270 | - # FIXME get_event_loop will be deprecated in python3.10 | 270 | + # FIXME: get_event_loop will be deprecated in python3.10 |
271 | loop = asyncio.get_event_loop() | 271 | loop = asyncio.get_event_loop() |
272 | for i, (qref, qfact) in enumerate(self["question_factory"].items()): | 272 | for i, (qref, qfact) in enumerate(self["question_factory"].items()): |
273 | try: | 273 | try: |
setup.py
@@ -22,15 +22,15 @@ setup( | @@ -22,15 +22,15 @@ setup( | ||
22 | url="https://git.xdi.uevora.pt/mjsb/perguntations.git", | 22 | url="https://git.xdi.uevora.pt/mjsb/perguntations.git", |
23 | packages=find_packages(), | 23 | packages=find_packages(), |
24 | include_package_data=True, # install files from MANIFEST.in | 24 | include_package_data=True, # install files from MANIFEST.in |
25 | - python_requires='>=3.8.*', | 25 | + python_requires='>=3.9', |
26 | install_requires=[ | 26 | install_requires=[ |
27 | - 'bcrypt>=3.1', | 27 | + 'argon2-cffi>=23.1', |
28 | 'mistune<2.0', | 28 | 'mistune<2.0', |
29 | 'pyyaml>=5.1', | 29 | 'pyyaml>=5.1', |
30 | 'pygments', | 30 | 'pygments', |
31 | 'schema>=0.7.5', | 31 | 'schema>=0.7.5', |
32 | - 'sqlalchemy>=1.4', | ||
33 | - 'tornado>=6.1', | 32 | + 'sqlalchemy>=2.0', |
33 | + 'tornado>=6.4', | ||
34 | ], | 34 | ], |
35 | entry_points={ | 35 | entry_points={ |
36 | 'console_scripts': [ | 36 | 'console_scripts': [ |