diff --git a/advlabdb/__init__.py b/advlabdb/__init__.py index d631093..ee302dd 100644 --- a/advlabdb/__init__.py +++ b/advlabdb/__init__.py @@ -9,8 +9,10 @@ from flask_debugtoolbar import DebugToolbarExtension app = Flask(__name__) app.debug = True #DEBUG +app.config["SERVER_NAME"] = "127.0.0.1:5000" #DEBUG + app.config["SECRET_KEY"] = "dev" -app.config['SECURITY_PASSWORD_SALT'] = "devSalt" # os.environ.get("SECURITY_PASSWORD_SALT", "") +app.config["SECURITY_PASSWORD_SALT"] = "devSalt" # os.environ.get("SECURITY_PASSWORD_SALT", "") app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///../advLab.db" db = SQLAlchemy(app) @@ -21,7 +23,7 @@ app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False #DEBUG DebugToolbarExtension(app) #DEBUG from advlabdb import customClasses -admin = Admin(app, name="Admin@AdvLabDB", template_mode='bootstrap3', index_view=customClasses.AdminIndexView()) +admin = Admin(app, name="Admin@AdvLabDB", template_mode='bootstrap3', index_view=customClasses.SecureAdminIndexView()) from advlabdb import models diff --git a/advlabdb/customClasses.py b/advlabdb/customClasses.py index 83e7f54..03cd178 100644 --- a/advlabdb/customClasses.py +++ b/advlabdb/customClasses.py @@ -1,13 +1,15 @@ -from flask_admin import AdminIndexView as OldAdminIndexView -from flask_admin.contrib.sqla import ModelView as OldModelView +from flask_admin import AdminIndexView +from flask_admin.contrib.sqla import ModelView from flask_security import current_user -class AdminIndexView(OldAdminIndexView): +class SecureAdminIndexView(AdminIndexView): def is_accessible(self): return current_user.has_role("admin") -class ModelView(OldModelView): +class SecureModelView(ModelView): + can_export = True + def is_accessible(self): return current_user.has_role("admin") diff --git a/advlabdb/modelViews.py b/advlabdb/modelViews.py index fe8a308..6e005d6 100644 --- a/advlabdb/modelViews.py +++ b/advlabdb/modelViews.py @@ -1,17 +1,53 @@ -from advlabdb import admin -from advlabdb.customClasses import ModelView -from advlabdb.models import * +from flask import url_for, render_template, flash +from advlabdb.customClasses import SecureModelView +from flask_admin.menu import MenuLink +from flask_security import hash_password +from wtforms import BooleanField -admin.add_view(ModelView(Student, db.session)) -admin.add_view(ModelView(PartStudent, db.session)) -admin.add_view(ModelView(Group, db.session)) -admin.add_view(ModelView(GroupExperiment, db.session)) -admin.add_view(ModelView(Experiment, db.session)) -admin.add_view(ModelView(PartExperiment, db.session)) -admin.add_view(ModelView(Assistant, db.session)) -admin.add_view(ModelView(Appointment, db.session)) -admin.add_view(ModelView(Part, db.session)) -admin.add_view(ModelView(Semester, db.session)) -admin.add_view(ModelView(ExperimentMark, db.session)) -admin.add_view(ModelView(User, db.session)) -admin.add_view(ModelView(Role, db.session)) +from advlabdb import admin, app, user_datastore +from advlabdb.models import * +from advlabdb.utils import randomPassword + +class UserModelView(SecureModelView): + column_list = ["email", "active", "roles", "assistant"] + column_searchable_list = ["email"] + column_filters = ["active"] + form_columns = ["email", "roles"] + + def create_model(self, form): + password = randomPassword() + passwordHash = hash_password(password) + + email = form.email.data.lower() + print(form.roles.data[0].name) + + roles = [role.name for role in form.roles.data] + if "admin" in roles: + flash("You have registered a new admin!", "danger") + + newUser = user_datastore.create_user(email=email, password=passwordHash, roles=roles) + db.session.commit() + flash(f"{email} registered with roles: {', '.join([role.name for role in form.roles.data])}.", category="success") + flash(f"Random password: {password}", category="warning") + return newUser + + +class RoleModelView(SecureModelView): + column_exclude_list = ["update_datetime"] + +admin.add_view(SecureModelView(Student, db.session)) +admin.add_view(SecureModelView(PartStudent, db.session)) +admin.add_view(SecureModelView(Group, db.session)) +admin.add_view(SecureModelView(GroupExperiment, db.session)) +admin.add_view(SecureModelView(Experiment, db.session)) +admin.add_view(SecureModelView(PartExperiment, db.session)) +admin.add_view(SecureModelView(Assistant, db.session)) +admin.add_view(SecureModelView(Appointment, db.session)) +admin.add_view(SecureModelView(Part, db.session)) +admin.add_view(SecureModelView(Semester, db.session)) +admin.add_view(SecureModelView(ExperimentMark, db.session)) +admin.add_view(UserModelView(User, db.session)) +admin.add_view(RoleModelView(Role, db.session)) + +with app.app_context(): + admin.add_link(MenuLink(name="Home", url=url_for("index"), category="Links")) diff --git a/advlabdb/models.py b/advlabdb/models.py index 7fd183a..fa4abcf 100644 --- a/advlabdb/models.py +++ b/advlabdb/models.py @@ -24,6 +24,9 @@ class Student(db.Model): note = db.Column(db.Text, nullable=True) part_students = db.relationship("PartStudent", backref="student", lazy=True) + def __repr__(self): + return f"<{self.first_name} {self.last_name}>" + class PartStudent(db.Model): # A student doing a specific part @@ -67,6 +70,9 @@ class Experiment(db.Model): final_weighting = db.Column(db.Float, nullable=False) part_experiments = db.relationship("PartExperiment", backref="experiment", lazy=True) + def __repr__(self): + return f"<{self.number}>" + # Helper table for the many to many relationship between Assistant and PartExperiment experiment_assistant = db.Table("experiment_assistant", @@ -99,6 +105,9 @@ class Assistant(db.Model): appointments = db.relationship("Appointment", backref="assistant", lazy=True) experiment_marks = db.relationship("ExperimentMark", backref="assistant", lazy=True) + def __repr__(self): + return f"<{self.first_name} {self.last_name}>" + class Appointment(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -122,6 +131,9 @@ class Semester(db.Model): label = db.Column(db.String(100), nullable=False) # WS2122 for example parts = db.relationship("Part", backref="semester", lazy=True) + def __repr__(self): + return f"<{self.label}>" + class ExperimentMark(db.Model): # A mark for a student after a specific experiment @@ -137,6 +149,10 @@ class ExperimentMark(db.Model): class User(db.Model, FsUserMixin): assistant = db.relationship("Assistant", backref="user", lazy=True, uselist=False) + def __repr__(self): + return f"<{self.email}>" + class Role(db.Model, FsRoleMixin): - pass + def __repr__(self): + return f"<{self.name}>" diff --git a/advlabdb/routes.py b/advlabdb/routes.py index adc3ff4..c9b50d8 100644 --- a/advlabdb/routes.py +++ b/advlabdb/routes.py @@ -177,30 +177,6 @@ def groups(): ) -@app.route("/users", methods=["GET", "POST"]) -@roles_required("admin") -def users(): - if request.method == 'POST': - if "registerUser" in request.form: - return redirect(url_for("register")) - else: - headerAndDataList = [["Email", "row.email"], - ["Roles", "[role.name for role in row.roles]"], - ["Assistant", "row.assistant"]] - - activeUsersTable = makeTable(headerAndDataList=headerAndDataList, - rows=User.query.filter(User.active == True).all(), - tableId="activeUsersTable") - inactiveUsersTable = makeTable(headerAndDataList=headerAndDataList, - rows=User.query.filter(User.active == False).all(), - tableId="inactiveUsersTable") - - return render_template("users.html", - activeUsersTable=activeUsersTable, - inactiveUsersTable=inactiveUsersTable, - ) - - @app.route("/deactivate_users", methods=["GET"]) @roles_required("admin") def deactivate_users(): @@ -241,42 +217,3 @@ def semesters(): def set_semester(): session["activeSemesterId"] = int(request.args.get("semester_id")) return redirect(request.referrer) - - -@app.route("/register", methods=["GET", "POST"]) -@roles_required("admin") -def register(): - form = RegistrationForm() - if form.validate_on_submit(): - password = randomPassword() - passwordHash = hash_password(password) - - email = form.email.data.lower() - assistant = form.assistant.data - admin = form.admin.data - - registered = True - if admin: - if assistant: - roles = ["admin", "assistant"] - else: - roles = ["admin"] - flash("You have registered a new admin!", "danger") - elif assistant: - roles = ["assistant"] - else: - flash("The user has to be assistant and/or admin!", "warning") - registered = False - - if registered: - newUser = user_datastore.create_user(email=email, password=passwordHash, roles=roles) - db.session.commit() - return render_template("registered.html", - email=email, - password=password, - roles=[role.name for role in newUser.roles], - ) - - return render_template("register.html", - form=form, - ) diff --git a/advlabdb/templates/register.html b/advlabdb/templates/register.html deleted file mode 100644 index 3961499..0000000 --- a/advlabdb/templates/register.html +++ /dev/null @@ -1,38 +0,0 @@ -{% set title = "Register" %} -{% extends "layout.html" %} -{% block content %} -
-
- {{form.hidden_tag()}} -
-
- {{form.email.label(class="form-control-label")}} - - {% if form.email.errors %} - {{form.email(class="form-control form-control-lg is-invalid")}} -
- {% for error in form.email.errors %} - {{ error }} - {% endfor %} -
- {% else %} - {{form.email(class="form-control form-control-lg")}} - {% endif %} -
-
- Roles -
- {{form.assistant(class="form-check-input")}} - {{form.assistant.label(class="form-check-label")}} -
- {{form.admin(class="form-check-input")}} - {{form.admin.label(class="form-check-label")}} -
-
-
-
- {{form.submit(class="btn btn-outline-info")}} -
-
-
-{% endblock content %} diff --git a/advlabdb/templates/registered.html b/advlabdb/templates/registered.html deleted file mode 100644 index 8bd8875..0000000 --- a/advlabdb/templates/registered.html +++ /dev/null @@ -1,12 +0,0 @@ -{% set title = "Registered" %} -{% extends "layout.html" %} -{% block content %} -

- New user registered with roles {{roles}}. -

-

- Email: {{email}} -
- Random password: {{password}} -

-{% endblock content %} diff --git a/advlabdb/templates/users.html b/advlabdb/templates/users.html deleted file mode 100644 index f61c5ef..0000000 --- a/advlabdb/templates/users.html +++ /dev/null @@ -1,36 +0,0 @@ -{% set title = "Users" %} -{% extends "layout.html" %} -{% block content %} - -

Active users:

-{{activeUsersTable|safe}} -
- -
-
-
- -
- -
-

Inactive users:

-{{inactiveUsersTable|safe}} -{% endblock content %} - -{% block scripts %} - -{% endblock scripts %} diff --git a/poetry.lock b/poetry.lock index 2b1510c..1568b41 100644 --- a/poetry.lock +++ b/poetry.lock @@ -258,7 +258,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "sqlalchemy" -version = "1.4.9" +version = "1.4.11" description = "Database Abstraction Library" category = "main" optional = false @@ -498,40 +498,40 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.4.9-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e26791ac43806dec1f18d328596db87f1b37f9d8271997dd1233054b4c377f51"}, - {file = "SQLAlchemy-1.4.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c4485040d86d4b3d9aa509fd3c492de3687d9bf52fb85d66b33912ad068a088c"}, - {file = "SQLAlchemy-1.4.9-cp27-cp27m-win32.whl", hash = "sha256:a8763fe4de02f746666161b130cc3e5d1494a6f5475f5622f05251739fc22e55"}, - {file = "SQLAlchemy-1.4.9-cp27-cp27m-win_amd64.whl", hash = "sha256:e7d262415e4adf148441bd9f10ae4e5498d6649962fabc62a64ec7b4891d56c5"}, - {file = "SQLAlchemy-1.4.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c6f228b79fd757d9ca539c9958190b3a44308f743dc7d83575aa0891033f6c86"}, - {file = "SQLAlchemy-1.4.9-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:cfbf2cf8e8ef0a1d23bfd0fa387057e6e522d55e43821f1d115941d913ee7762"}, - {file = "SQLAlchemy-1.4.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:815a8cdf9c0fa504d0bfbe83fb3e596b7663fc828b73259a20299c01330467aa"}, - {file = "SQLAlchemy-1.4.9-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:cfa4a336de7d32ae30b54f7b8ec888fb5c6313a1b7419a9d7b3f49cdd83012a3"}, - {file = "SQLAlchemy-1.4.9-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:065ac7331b87494a86bf3dc4430c1ee7779d6dc532213c528394ddd00804e518"}, - {file = "SQLAlchemy-1.4.9-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:690fbca2a208314504a2ab46d3e7dae320247fcb1967863b9782a70bf49fc600"}, - {file = "SQLAlchemy-1.4.9-cp36-cp36m-win32.whl", hash = "sha256:4edff2b4101a1c442fb1b17d594a5fdf99145f27c5eaffae12c26aef2bb2bf65"}, - {file = "SQLAlchemy-1.4.9-cp36-cp36m-win_amd64.whl", hash = "sha256:6c6090d73820dcf04549f0b6e80f67b46c8191f0e40bf09c6d6f8ece2464e8b6"}, - {file = "SQLAlchemy-1.4.9-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:fc82688695eacf77befc3d839df2bc7ff314cd1d547f120835acdcbac1a480b8"}, - {file = "SQLAlchemy-1.4.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4e554872766d2783abf0a11704536596e8794229fb0fa63d311a74caae58c6c5"}, - {file = "SQLAlchemy-1.4.9-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bce6eaf7b9a3a445911e225570b8fd26b7e98654ac9f308a8a52addb64a2a488"}, - {file = "SQLAlchemy-1.4.9-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:25aaf0bec9eadde9789e3c0178c718ae6923b57485fdeae85999bc3089d9b871"}, - {file = "SQLAlchemy-1.4.9-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:f239778cf03cd46da4962636501f6dea55af9b4684cd7ceee104ad4f0290e878"}, - {file = "SQLAlchemy-1.4.9-cp37-cp37m-win32.whl", hash = "sha256:b0266e133d819d33b555798822606e876187a96798e2d8c9b7f85e419d73ef94"}, - {file = "SQLAlchemy-1.4.9-cp37-cp37m-win_amd64.whl", hash = "sha256:230b210fc6d1af5d555d1d04ff9bd4259d6ab82b020369724ab4a1c805a32dd3"}, - {file = "SQLAlchemy-1.4.9-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a28c7b96bc5beef585172ca9d79068ae7fa2527feaa26bd63371851d7894c66f"}, - {file = "SQLAlchemy-1.4.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:457a1652bc1c5f832165ff341380b3742bfb98b9ceca24576350992713ad700f"}, - {file = "SQLAlchemy-1.4.9-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:e9e95568eafae18ac40d00694b82dc3febe653f81eee83204ef248563f39696d"}, - {file = "SQLAlchemy-1.4.9-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0d8aab144cf8d31c1ac834802c7df4430248f74bd8b3ed3149f9c9eec0eafe50"}, - {file = "SQLAlchemy-1.4.9-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:cde2cf3ee76e8c538f2f43f5cf9252ad53404fc350801191128bab68f335a8b2"}, - {file = "SQLAlchemy-1.4.9-cp38-cp38-win32.whl", hash = "sha256:bb97aeaa699c43da62e35856ab56e5154d062c09a3593a2c12c67d6a21059920"}, - {file = "SQLAlchemy-1.4.9-cp38-cp38-win_amd64.whl", hash = "sha256:fbdcf9019e92253fc6aa0bcd5937302664c3a4d53884c425c0caa994e56c4421"}, - {file = "SQLAlchemy-1.4.9-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2e1b8d31c97a2b91aea8ed8299ad360a32d60728a89f2aac9c98eef07a633a0e"}, - {file = "SQLAlchemy-1.4.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7bdb0f972bc35054c05088e91cec8fa810c3aa565b690bae75c005ee430e12e8"}, - {file = "SQLAlchemy-1.4.9-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ec7c33e22beac16b4c5348c41cd94cfee056152e55a0efc62843deebfc53fcb4"}, - {file = "SQLAlchemy-1.4.9-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:29816a338982c30dd7ee76c4e79f17d5991abb1b6561e9f1d72703d030a79c86"}, - {file = "SQLAlchemy-1.4.9-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:099e63ffad329989080c533896267c40f9cb38ed5704168f7dae3afdda121e10"}, - {file = "SQLAlchemy-1.4.9-cp39-cp39-win32.whl", hash = "sha256:343c679899afdc4952ac659dc46f2075a2bd4fba87ca0df264be838eecd02096"}, - {file = "SQLAlchemy-1.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:386f215248c3fb2fab9bb77f631bc3c6cd38354ca2363d241784f8297d16b80a"}, - {file = "SQLAlchemy-1.4.9.tar.gz", hash = "sha256:f31757972677fbe9132932a69a4f23db59187a072cc26427f56a3082b46b6dac"}, + {file = "SQLAlchemy-1.4.11-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:b8b7d66ee8b8ac272adce0af1342a60854f0d89686e6d3318127a6a82a2f765c"}, + {file = "SQLAlchemy-1.4.11-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:03a503ecff0cc2be3ad4dafd220eaff13721edb11c191670b7662932fb0a5c3a"}, + {file = "SQLAlchemy-1.4.11-cp27-cp27m-win32.whl", hash = "sha256:9cf94161cb55507cee147bf8abcfd3c076b353ad18743296764dd81108ea74f8"}, + {file = "SQLAlchemy-1.4.11-cp27-cp27m-win_amd64.whl", hash = "sha256:d08173144aebdf30c21a331b532db16535cfa83deed12e8703fa6c67c0894ffc"}, + {file = "SQLAlchemy-1.4.11-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:31e941d6db8b026bc63e46ef71e877913f128bd44260b90c645432626b7f9a47"}, + {file = "SQLAlchemy-1.4.11-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:1e14fa32969badef9c309f55352e5c46f321bd29f7c600556caacdaa3eddfcf6"}, + {file = "SQLAlchemy-1.4.11-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6389b10e23329dc8b5600c1a84e3da2628d0f437d8a5cd05aefd1470ec571dd1"}, + {file = "SQLAlchemy-1.4.11-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4f631edf45a943738fa77612e85fc5c5d3fb637c4f5a530f7eedd1a7cd7a70a7"}, + {file = "SQLAlchemy-1.4.11-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4a7d4da2acf6d5d068fb41c48950827c49c3c68bfb46a1da45ea8fbf7ed4b471"}, + {file = "SQLAlchemy-1.4.11-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:f772e4428d413c0affe2a34836278fbe9df9a9c0940705860c2d3a4b50af1a66"}, + {file = "SQLAlchemy-1.4.11-cp36-cp36m-win32.whl", hash = "sha256:0140f6dac2659fa6783e7029085ab0447d8eb23cf4d831fb907588d27ba158f7"}, + {file = "SQLAlchemy-1.4.11-cp36-cp36m-win_amd64.whl", hash = "sha256:7d89add44938ea4f52c7641d5805c9e154fed4381e874ef3221483eeb191a96d"}, + {file = "SQLAlchemy-1.4.11-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:452c4e002be727cb6f929dbd32bbc666a0921b86555b8af09709060ed3954bd3"}, + {file = "SQLAlchemy-1.4.11-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:069de3a701d33709236efe0d06f38846b738b19c63d45cc47f54590982ba7802"}, + {file = "SQLAlchemy-1.4.11-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bb1072fdf48ba870c0fe81bee8babe4ba2f096fb56bb4f3e0c2386a7626e405c"}, + {file = "SQLAlchemy-1.4.11-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8f96d4b6a49d3f0f109365bb6303ae5d266d3f90280ca68cf8b2c46032491038"}, + {file = "SQLAlchemy-1.4.11-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:98214f04802a3fc740038744d8981a8f2fdca710f791ca125fc4792737d9f3a7"}, + {file = "SQLAlchemy-1.4.11-cp37-cp37m-win32.whl", hash = "sha256:9fdf0713166f33e5e6ea98cf59deb305cb323131277f6880de6c509f468076f8"}, + {file = "SQLAlchemy-1.4.11-cp37-cp37m-win_amd64.whl", hash = "sha256:6ebd58e73b7bd902688c0bb8dbabb0c36b756f02cc7b27ad5efa2f380c611f95"}, + {file = "SQLAlchemy-1.4.11-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a41ab83ecfadf38a47bdfaf4e488f71579df47a711e1ab1dce30d34c7c25bd00"}, + {file = "SQLAlchemy-1.4.11-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:034b42a6a59bf4ddc57e5a38a9dbac83ccd94c0b565ba91dba4ff58149706028"}, + {file = "SQLAlchemy-1.4.11-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1735e06a3d5b0793d5ee2d952df8a5c63edaff6383c2210c9b5c93dc2ea4c315"}, + {file = "SQLAlchemy-1.4.11-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:96de1d4a2e05d4a017087cb29cd6a8ebfeecfd0e9f872880b1a589f011c1c02e"}, + {file = "SQLAlchemy-1.4.11-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:7180830ea1082b96b94884bc352b274e29b45151b6ee911bf1fd79cba2de659b"}, + {file = "SQLAlchemy-1.4.11-cp38-cp38-win32.whl", hash = "sha256:961b089e64c2ad29ad367487dd3ba1aa3eeba56bc82037ce91732baaa0f6ca90"}, + {file = "SQLAlchemy-1.4.11-cp38-cp38-win_amd64.whl", hash = "sha256:19633df6be629200ff3c026f2837e1dd17908fb1bcea860290a5a45e6fa5148e"}, + {file = "SQLAlchemy-1.4.11-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:789be639501445d85fd4ca41d04f0f5c6cbb6deb0c6826aaa6f22774fe84ef94"}, + {file = "SQLAlchemy-1.4.11-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:ac14fee167653ec6dee32d6aa4d501d90ae1bfbbc3eb5816940bccf227f0d617"}, + {file = "SQLAlchemy-1.4.11-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:cd823071b97c1a6ac3af9e43b5d861126a1304033dcd18dfe354a02ec45642fe"}, + {file = "SQLAlchemy-1.4.11-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:842b0d4698381aac047f8ae57409c90b7e63ebabf5bc02814ddc8eaefd13499e"}, + {file = "SQLAlchemy-1.4.11-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:45a720029756800628359192630fffdc9660ab6f27f0409bd24d9e09d75d6c18"}, + {file = "SQLAlchemy-1.4.11-cp39-cp39-win32.whl", hash = "sha256:e7d76312e904aa4ea221a92c0bc2e299ad46e4580e2d72ca1f7e6d31dce5bfab"}, + {file = "SQLAlchemy-1.4.11-cp39-cp39-win_amd64.whl", hash = "sha256:4a2e7f037d3ca818d6d0490e3323fd451545f580df30d62b698da2f247015a34"}, + {file = "SQLAlchemy-1.4.11.tar.gz", hash = "sha256:4ad4044eb86fbcbdff2106e44f479fbdac703d77860b3e19988c8a8786e73061"}, ] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},