diff --git a/aprendizations/initdb.py b/aprendizations/initdb.py index 11c99bf..b906a77 100644 --- a/aprendizations/initdb.py +++ b/aprendizations/initdb.py @@ -9,13 +9,12 @@ import csv import argparse import re from string import capwords -from concurrent.futures import ThreadPoolExecutor # third party libraries import bcrypt -import sqlalchemy as sa -import sqlalchemy.orm as orm -from sqlalchemy.exc import IntegrityError +from sqlalchemy import create_engine, select +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError, NoResultFound # this project from aprendizations.models import Base, Student @@ -103,24 +102,14 @@ def get_students_from_csv(filename): # =========================================================================== -def hashpw(student, passw=None): - '''replace password by hash for a single student''' - print('.', end='', flush=True) - passw = (passw or student.get('pw', None) or student['uid']).encode('utf-8') - student['pw'] = bcrypt.hashpw(passw, bcrypt.gensalt()) - - -# =========================================================================== def show_students_in_database(session, verbose=False): - '''print students that are in the database''' - users = session.query(Student).all() + '''shows students in the database''' + users = session.execute(select(Student)).scalars().all() total = len(users) print('\nRegistered users:') - if total == 0: - print(' -- none --') - else: - users.sort(key=lambda u: f'{u.id:>12}') # sort by number + if users: + users.sort(key=lambda u: f'{u.id:>12}') # sort by right aligned string if verbose: for user in users: print(f'{user.id:>12} {user.name}') @@ -142,65 +131,64 @@ def main(): args = parse_commandline_arguments() # --- database stuff - print(f'Using database: {args.db}') - engine = sa.create_engine(f'sqlite:///{args.db}', echo=False) + print(f'Database: {args.db}') + engine = create_engine(f'sqlite:///{args.db}', echo=False, future=True) Base.metadata.create_all(engine) # Creates schema if needed - session = orm.sessionmaker(bind=engine)() + session = Session(engine) # --- build list of students to insert/update students = [] for csvfile in args.csvfile: - # print('Adding users from:', csvfile) - students.extend(get_students_from_csv(csvfile)) + students += get_students_from_csv(csvfile) if args.admin: - # print('Adding user: 0, Admin.') students.append({'uid': '0', 'name': 'Admin'}) if args.add: for uid, name in args.add: - # print(f'Adding user: {uid}, {name}.') students.append({'uid': uid, 'name': name}) # --- only insert students that are not yet in the database - db_students = {user.id for user in session.query(Student).all()} - new_students = list(filter(lambda s: s['uid'] not in db_students, students)) - - if new_students: - # --- password hashing - print('Generating password hashes', end='') - with ThreadPoolExecutor() as executor: - executor.map(lambda s: hashpw(s, args.pw), new_students) - - print('\nAdding students:') - for student in new_students: - print(f' + {student["uid"]}, {student["name"]}') - - try: - session.add_all([Student(id=s['uid'], - name=s['name'], - password=s['pw']) - for s in new_students]) - session.commit() - except IntegrityError: - print('!!! Integrity error. Aborted !!!\n') - session.rollback() - - print(f'Inserted {len(new_students)} new student(s).') + print('\nInserting new students:') + + db_students = set(session.execute(select(Student.id)).scalars().all()) + new_students = (s for s in students if s['uid'] not in db_students) + count = 0 + for s in new_students: + print(f' {s["uid"]}, {s["name"]}') + + pw = args.pw or s['uid'] + hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + + session.add(Student(id=s['uid'], name=s['name'], password=hashed_pw)) + count += 1 + + try: + session.commit() + except IntegrityError: + print('!!! Integrity error. Aborted !!!\n') + session.rollback() else: - print('There are no new students to add.') + print(f'Total {count} new student(s).') # --- update data for student in the database - for student_id in args.update: - print(f'Updating password of: {student_id}') - student = session.query(Student).get(student_id) - if student is not None: - passw = (args.pw or student_id).encode('utf-8') - student.password = bcrypt.hashpw(passw, bcrypt.gensalt()) - session.commit() - else: - print(f'!!! Student {student_id} does not exist. Skipped!!!') + if args.update: + print('\nUpdating passwords of students:') + count = 0 + for sid in args.update: + try: + s = session.execute(select(Student).filter_by(id=sid)).scalar_one() + except NoResultFound: + print(f' -> student {sid} does not exist!') + continue + else: + print(f' {sid}, {s.name}') + count += 1 + pw = args.pw or sid + s.password = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + session.commit() + print(f'Total {count} password(s) updated.') show_students_in_database(session, args.verbose) diff --git a/aprendizations/models.py b/aprendizations/models.py index 74069b5..9beef48 100644 --- a/aprendizations/models.py +++ b/aprendizations/models.py @@ -1,17 +1,17 @@ # python standard library -from typing import Any +# from typing import Any # third party libraries from sqlalchemy import Column, ForeignKey, Integer, Float, String -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship # =========================================================================== # Declare ORM # FIXME Any is a workaround for mypy static type checking (see https://github.com/python/mypy/issues/6372) -Base: Any = declarative_base() +# Base: Any = declarative_base() +Base = declarative_base() # --------------------------------------------------------------------------- @@ -27,11 +27,11 @@ class StudentTopic(Base): topic = relationship('Topic', back_populates='students') def __repr__(self): - return f'''StudentTopic: - student_id: "{self.student_id}" - topic_id: "{self.topic_id}" - level: "{self.level}" - date: "{self.date}"''' + return ('StudentTopic(' + f'student_id={self.student_id!r}, ' + f'topic_id={self.topic_id!r}, ' + f'level={self.level!r}, ' + f'date={self.date!r})') # --------------------------------------------------------------------------- @@ -48,10 +48,10 @@ class Student(Base): topics = relationship('StudentTopic', back_populates='student') def __repr__(self): - return f'''Student: - id: "{self.id}" - name: "{self.name}" - password: "{self.password}"''' + return ('Student(' + f'id={self.id!r}, ' + f'name={self.name!r}, ' + f'password={self.password!r})') # --------------------------------------------------------------------------- @@ -72,14 +72,14 @@ class Answer(Base): topic = relationship('Topic', back_populates='answers') def __repr__(self): - return f'''Question: - id: "{self.id}" - ref: "{self.ref}" - grade: "{self.grade}" - starttime: "{self.starttime}" - finishtime: "{self.finishtime}" - student_id: "{self.student_id}" - topic_id: "{self.topic_id}"''' + return ('Question(' + f'id={self.id!r}, ' + f'ref={self.ref!r}, ' + f'grade={self.grade!r}, ' + f'starttime={self.starttime!r}, ' + f'finishtime={self.finishtime!r}, ' + f'student_id={self.student_id!r}, ' + f'topic_id={self.topic_id!r})') # --------------------------------------------------------------------------- @@ -94,5 +94,4 @@ class Topic(Base): answers = relationship('Answer', back_populates='topic') def __repr__(self): - return f'''Topic: - id: "{self.id}"''' + return f'Topic(id={self.id!r})' diff --git a/mypy.ini b/mypy.ini index e7ab926..b4b39f8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,10 +1,7 @@ [mypy] -python_version = 3.8 -warn_return_any = True -warn_unused_configs = True +python_version = 3.9 +plugins = sqlalchemy.ext.mypy.plugin -[mypy-sqlalchemy.*] -ignore_missing_imports = True [mypy-pygments.*] ignore_missing_imports = True diff --git a/package-lock.json b/package-lock.json index 2118286..a4e27a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,8 +5,8 @@ "packages": { "": { "dependencies": { - "@fortawesome/fontawesome-free": "^5.15.2", - "codemirror": "^5.59.1", + "@fortawesome/fontawesome-free": "^5.15.3", + "codemirror": "^5.59.4", "mdbootstrap": "^4.19.2" } }, @@ -20,9 +20,9 @@ } }, "node_modules/codemirror": { - "version": "5.61.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz", - "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==" + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.0.tgz", + "integrity": "sha512-Xnl3304iCc8nyVZuRkzDVVwc794uc9QNX0UcPGeNic1fbzkSrO4l4GVXho9tRNKBgPYZXgocUqXyfIv3BILhCQ==" }, "node_modules/mdbootstrap": { "version": "4.19.2", @@ -32,11 +32,14 @@ }, "dependencies": { "@fortawesome/fontawesome-free": { + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", + "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" }, "codemirror": { - "version": "5.61.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz", - "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==" + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.0.tgz", + "integrity": "sha512-Xnl3304iCc8nyVZuRkzDVVwc794uc9QNX0UcPGeNic1fbzkSrO4l4GVXho9tRNKBgPYZXgocUqXyfIv3BILhCQ==" }, "mdbootstrap": { "version": "4.19.2", diff --git a/setup.py b/setup.py index 109dd11..26e5bd8 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( 'mistune', 'pyyaml>=5.1', 'pygments', - 'sqlalchemy<1.4', + 'sqlalchemy>=1.4', 'bcrypt>=3.1', 'networkx>=2.4' ], -- libgit2 0.21.2