diff --git a/gallery/__init__.py b/gallery/__init__.py index 774b119..6ec3f26 100644 --- a/gallery/__init__.py +++ b/gallery/__init__.py @@ -8,14 +8,12 @@ print(""" Created by Fluffy Bean - Version 23.03.02 """) - from flask import Flask, render_template from flask_compress import Compress from dotenv import load_dotenv import platformdirs -# Load logger from gallery.logger import logger logger.innit_logger() @@ -25,7 +23,7 @@ import os # Check if any of the required files are missing if not os.path.exists(platformdirs.user_config_dir('onlylegs')): - from setup import setup + from .setup import setup setup() @@ -78,14 +76,12 @@ def create_app(test_config=None): except OSError: pass - # Load database - from . import db - db.init_app(app) # Load theme from . import sassy sassy.compile('default', app.root_path) + @app.errorhandler(405) def method_not_allowed(e): error = '405' diff --git a/gallery/api.py b/gallery/api.py index bac571d..29f56bf 100644 --- a/gallery/api.py +++ b/gallery/api.py @@ -2,7 +2,11 @@ from flask import Blueprint, current_app, send_from_directory, send_file, reques from werkzeug.utils import secure_filename from gallery.auth import login_required -from gallery.db import get_db + +from . import db +from sqlalchemy.orm import sessionmaker +db_session = sessionmaker(bind=db.engine) +db_session = db_session() from PIL import Image, ImageOps, ImageFilter from . import metadata as mt @@ -104,11 +108,9 @@ def upload(): # Save to database try: - db = get_db() - db.execute( - 'INSERT INTO posts (file_name, author_id, description, alt)' - ' VALUES (?, ?, ?, ?)', - (img_name, g.user['id'], form['description'], form['alt'])) + tr = db.posts(img_name, form['description'], form['alt'], g.user.id) + db_session.add(tr) + db_session.commit() except Exception as e: logger.server(600, f"Error saving to database: {e}") abort(500) @@ -117,7 +119,6 @@ def upload(): try: form_file.save( os.path.join(current_app.config['UPLOAD_FOLDER'], img_name)) - db.commit() except Exception as e: logger.server(600, f"Error saving file: {e}") abort(500) @@ -128,28 +129,25 @@ def upload(): @blueprint.route('/remove/', methods=['POST']) @login_required def remove(id): - img = get_db().execute( - 'SELECT author_id, file_name FROM posts WHERE id = ?', - (id, )).fetchone() + img = db_session.query(db.posts).filter_by(id=id).first() if img is None: abort(404) - if img['author_id'] != g.user['id']: + if img.author_id != g.user.id: abort(403) try: os.remove( os.path.join(current_app.config['UPLOAD_FOLDER'], - img['file_name'])) + img.file_name)) except Exception as e: logger.server(600, f"Error removing file: {e}") abort(500) try: - db = get_db() - db.execute('DELETE FROM posts WHERE id = ?', (id, )) - db.commit() - except: + db_session.query(db.posts).filter_by(id=id).delete() + db_session.commit() + except Exception as e: logger.server(600, f"Error removing from database: {e}") abort(500) @@ -160,15 +158,13 @@ def remove(id): @blueprint.route('/metadata/', methods=['GET']) def metadata(id): - img = get_db().execute( - 'SELECT file_name, description, alt FROM posts WHERE id = ?', - (id, )).fetchone() + img = db_session.query(db.posts).filter_by(id=id).first() if img is None: abort(404) exif = mt.metadata.yoink( - os.path.join(current_app.config['UPLOAD_FOLDER'], img['file_name'])) + os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)) return jsonify(exif) diff --git a/gallery/auth.py b/gallery/auth.py index 6ab9968..de5db08 100644 --- a/gallery/auth.py +++ b/gallery/auth.py @@ -2,7 +2,10 @@ import functools from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify, current_app from werkzeug.security import check_password_hash, generate_password_hash -from gallery.db import get_db +from gallery import db +from sqlalchemy.orm import sessionmaker +db_session = sessionmaker(bind=db.engine) +db_session = db_session() from .logger import logger @@ -12,45 +15,26 @@ import uuid blueprint = Blueprint('auth', __name__, url_prefix='/auth') -# def add_log(code, note=None): -# code = int(code) -# note = str(note) - -# user_id = session.get('user_id') -# user_ip = request.remote_addr -# db = get_db() - -# db.execute( -# 'INSERT INTO logs (ip, user_id, code, note)' -# ' VALUES (?, ?, ?, ?)', -# (user_ip, user_id, code, note) -# ) -# db.commit() - - @blueprint.before_app_request def load_logged_in_user(): user_id = session.get('user_id') user_uuid = session.get('uuid') if user_id is None or user_uuid is None: - # This is not needed as the user is not logged in anyway, also spams the logs + # This is not needed as the user is not logged in anyway, also spams the server logs with useless data #add_log(103, 'Auth error before app request') g.user = None session.clear() else: - db = get_db() - is_alive = db.execute('SELECT * FROM devices WHERE session_uuid = ?', - (session.get('uuid'), )).fetchone() + is_alive = db_session.query(db.sessions).filter_by(session_uuid=user_uuid).first() if is_alive is None: logger.add(103, 'Session expired') flash(['Session expired!', '3']) session.clear() else: - g.user = db.execute('SELECT * FROM users WHERE id = ?', - (user_id, )).fetchone() - + g.user = db_session.query(db.users).filter_by(id=user_id).first() + @blueprint.route('/register', methods=['POST']) def register(): @@ -58,7 +42,6 @@ def register(): email = request.form['email'] password = request.form['password'] password_repeat = request.form['password-repeat'] - db = get_db() error = [] if not username: @@ -82,12 +65,10 @@ def register(): if not error: try: - db.execute( - 'INSERT INTO users (username, email, password) VALUES (?, ?, ?)', - (username, email, generate_password_hash(password)), - ) - db.commit() - except db.IntegrityError: + tr = db.users(username, email, generate_password_hash(password)) + db_session.add(tr) + db_session.commit() + except Exception as e: error.append(f"User {username} is already registered!") else: logger.add(103, f"User {username} registered") @@ -100,27 +81,24 @@ def register(): def login(): username = request.form['username'] password = request.form['password'] - db = get_db() error = None - user = db.execute('SELECT * FROM users WHERE username = ?', - (username, )).fetchone() + user = db_session.query(db.users).filter_by(username=username).first() if user is None: logger.add(101, f"User {username} does not exist from {request.remote_addr}") abort(403) - elif not check_password_hash(user['password'], password): + elif not check_password_hash(user.password, password): logger.add(102, f"User {username} password error from {request.remote_addr}") abort(403) try: session.clear() - session['user_id'] = user['id'] + session['user_id'] = user.id session['uuid'] = str(uuid.uuid4()) - - db.execute( - 'INSERT INTO devices (user_id, session_uuid, ip) VALUES (?, ?, ?)', - (user['id'], session.get('uuid'), request.remote_addr)) - db.commit() + + tr = db.sessions(user.id, session.get('uuid'), request.remote_addr, request.user_agent.string, 1) + db_session.add(tr) + db_session.commit() except error as err: logger.add(105, f"User {username} auth error: {err}") abort(500) @@ -135,7 +113,7 @@ def login(): @blueprint.route('/logout') def logout(): - logger.add(103, f"User {g.user['username']} - id: {g.user['id']} logged out") + logger.add(103, f"User {g.user.username} - id: {g.user.id} logged out") session.clear() return redirect(url_for('index')) diff --git a/gallery/db.py b/gallery/db.py index b4242d1..6bef906 100644 --- a/gallery/db.py +++ b/gallery/db.py @@ -1,38 +1,133 @@ -import sqlite3 -import click -from flask import current_app, g +import os +import platformdirs +from datetime import datetime + +from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import declarative_base, relationship -@click.command('init-db') -def init_db_command(): - """Create tables if not already created""" - init_db() - click.echo('Initialized the database!') +path_to_db = os.path.join(platformdirs.user_config_dir('onlylegs'), 'gallery.sqlite') +engine = create_engine(f'sqlite:///{path_to_db}', echo=False) +base = declarative_base() -def get_db(): - if 'db' not in g: - g.db = sqlite3.connect(current_app.config['DATABASE'], - detect_types=sqlite3.PARSE_DECLTYPES) - g.db.row_factory = sqlite3.Row +class users (base): + __tablename__ = 'users' - return g.db + id = Column(Integer, primary_key=True) + username = Column(String, unique=True, nullable=False) + email = Column(String, unique=True, nullable=False) + password = Column(String, nullable=False) + created_at = Column(DateTime, nullable=False) + + posts = relationship('posts') + groups = relationship('groups') + session = relationship('sessions') + log = relationship('logs') + + def __init__(self, username, email, password): + self.username = username + self.email = email + self.password = password + self.created_at = datetime.now() + +class posts (base): + __tablename__ = 'posts' + + id = Column(Integer, primary_key=True) + file_name = Column(String, unique=True, nullable=False) + description = Column(String, nullable=False) + alt = Column(String, nullable=False) + author_id = Column(Integer, ForeignKey('users.id')) + created_at = Column(DateTime, nullable=False) + + junction = relationship('group_junction') + + def __init__(self, file_name, description, alt, author_id): + self.file_name = file_name + self.description = description + self.alt = alt + self.author_id = author_id + self.created_at = datetime.now() + +class groups (base): + __tablename__ = 'groups' + + id = Column(Integer, primary_key=True) + name = Column(String, nullable=False) + description = Column(String, nullable=False) + author_id = Column(Integer, ForeignKey('users.id')) + created_at = Column(DateTime, nullable=False) + + junction = relationship('group_junction') + + def __init__(self, name, description, author_id): + self.name = name + self.description = description + self.author_id = author_id + self.created_at = datetime.now() + +class group_junction (base): + __tablename__ = 'group_junction' + + id = Column(Integer, primary_key=True) + group_id = Column(Integer, ForeignKey('groups.id')) + post_id = Column(Integer, ForeignKey('posts.id')) + + def __init__(self, group_id, post_id): + self.group_id = group_id + self.post_id = post_id + +class sessions (base): + __tablename__ = 'sessions' + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id')) + session_uuid = Column(String, nullable=False) + ip = Column(String, nullable=False) + user_agent = Column(String, nullable=False) + active = Column(Boolean, nullable=False) + created_at = Column(DateTime, nullable=False) + + def __init__(self, user_id, session_uuid, ip, user_agent, active): + self.user_id = user_id + self.session_uuid = session_uuid + self.ip = ip + self.user_agent = user_agent + self.active = active + self.created_at = datetime.now() + +class logs (base): + __tablename__ = 'logs' + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id')) + ip = Column(String, nullable=False) + code = Column(Integer, nullable=False) + msg = Column(String, nullable=False) + created_at = Column(DateTime, nullable=False) + + def __init__(self, user_id, ip, code, msg): + self.user_id = user_id + self.ip = ip + self.code = code + self.msg = msg + self.created_at = datetime.now() + +class bans (base): + __tablename__ = 'bans' + + id = Column(Integer, primary_key=True) + ip = Column(String, nullable=False) + code = Column(Integer, nullable=False) + msg = Column(String, nullable=False) + created_at = Column(DateTime, nullable=False) + + def __init__(self, ip, code, msg): + self.ip = ip + self.code = code + self.msg = msg + self.created_at = datetime.now() -def close_db(e=None): - db = g.pop('db', None) - - if db is not None: - db.close() - - -def init_db(): - db = get_db() - - with current_app.open_resource('schema.sql') as f: - db.executescript(f.read().decode('utf8')) - - -def init_app(app): - app.teardown_appcontext(close_db) - app.cli.add_command(init_db_command) \ No newline at end of file +base.metadata.create_all(engine) \ No newline at end of file diff --git a/gallery/routing.py b/gallery/routing.py index adeea9d..88b2f98 100644 --- a/gallery/routing.py +++ b/gallery/routing.py @@ -3,23 +3,22 @@ from werkzeug.exceptions import abort from werkzeug.utils import secure_filename from gallery.auth import login_required -from gallery.db import get_db + +from . import db +from sqlalchemy.orm import sessionmaker +db_session = sessionmaker(bind=db.engine) +db_session = db_session() from . import metadata as mt -from PIL import Image import os -from datetime import datetime -dt = datetime.now() blueprint = Blueprint('gallery', __name__) @blueprint.route('/') def index(): - db = get_db() - images = db.execute('SELECT * FROM posts' - ' ORDER BY created_at DESC').fetchall() + images = db_session.query(db.posts).order_by(db.posts.id.desc()).all() return render_template('index.html', images=images, @@ -30,17 +29,15 @@ def index(): @blueprint.route('/image/') def image(id): - # Get image from database - db = get_db() - image = db.execute('SELECT * FROM posts WHERE id = ?', (id, )).fetchone() + img = db_session.query(db.posts).filter_by(id=id).first() - if image is None: + if img is None: abort(404) exif = mt.metadata.yoink( - os.path.join(current_app.config['UPLOAD_FOLDER'], image['file_name'])) + os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)) - return render_template('image.html', image=image, exif=exif) + return render_template('image.html', image=img, exif=exif) @blueprint.route('/group') diff --git a/gallery/schema.sql b/gallery/schema.sql deleted file mode 100644 index 73d44a8..0000000 --- a/gallery/schema.sql +++ /dev/null @@ -1,78 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, - profile_picture TEXT NOT NULL DEFAULT 'default.png', - email TEXT NOT NULL, - password TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS posts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - file_name TEXT NOT NULL UNIQUE, - author_id INTEGER NOT NULL, - description TEXT NOT NULL, - alt TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (author_id) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS groups ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - author_id INTEGER NOT NULL, - name TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (author_id) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS group_junction ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - group_id INTEGER NOT NULL, - image_id INTEGER NOT NULL, - FOREIGN KEY (group_id) REFERENCES groups (id), - FOREIGN KEY (image_id) REFERENCES posts (id) -); - -CREATE TABLE IF NOT EXISTS permissions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - admin BOOLEAN NOT NULL DEFAULT FALSE, - create_posts BOOLEAN NOT NULL DEFAULT TRUE, - FOREIGN KEY (user_id) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS devices ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - session_uuid TEXT NOT NULL, - ip TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS tokens ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - token TEXT NOT NULL UNIQUE, - is_used BOOLEAN NOT NULL DEFAULT FALSE, - used_by INTEGER DEFAULT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (used_by) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - ip TEXT NOT NULL, - user_id INTEGER DEFAULT NULL, - code INTEGER NOT NULL, - note TEXT DEFAULT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS bans ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - ip TEXT NOT NULL, - code INTEGER NOT NULL, - note TEXT DEFAULT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); diff --git a/gallery/settings.py b/gallery/settings.py index 8596923..9d01c1a 100644 --- a/gallery/settings.py +++ b/gallery/settings.py @@ -2,7 +2,6 @@ from flask import Blueprint, render_template, url_for from werkzeug.exceptions import abort from gallery.auth import login_required -from gallery.db import get_db from datetime import datetime diff --git a/gallery/static/images/icon.png b/gallery/static/images/icon.png new file mode 100644 index 0000000..2b1b27c Binary files /dev/null and b/gallery/static/images/icon.png differ diff --git a/gallery/templates/layout.html b/gallery/templates/layout.html index 5e9b575..fcd3d35 100644 --- a/gallery/templates/layout.html +++ b/gallery/templates/layout.html @@ -4,6 +4,7 @@ Gallery + diff --git a/poetry.lock b/poetry.lock index 052c208..0a09bdd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -172,6 +172,80 @@ files = [ brotli = "*" flask = "*" +[[package]] +name = "greenlet" +version = "2.0.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +files = [ + {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, + {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, + {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, + {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, + {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, + {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, + {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, + {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, + {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, + {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, + {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, + {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, + {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, + {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, + {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, + {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, + {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, + {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, + {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, + {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, + {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, + {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, +] + +[package.extras] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] + [[package]] name = "gunicorn" version = "20.1.0" @@ -487,6 +561,96 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "sqlalchemy" +version = "2.0.4" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b67d6e626caa571fb53accaac2fba003ef4f7317cb3481e9ab99dad6e89a70d6"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b01dce097cf6f145da131a53d4cce7f42e0bfa9ae161dd171a423f7970d296d0"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738c80705e11c1268827dbe22c01162a9cdc98fc6f7901b429a1459db2593060"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6363697c938b9a13e07f1bc2cd433502a7aa07efd55b946b31d25b9449890621"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a42e6831e82dfa6d16b45f0c98c69e7b0defc64d76213173456355034450c414"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:011ef3c33f30bae5637c575f30647e0add98686642d237f0c3a1e3d9b35747fa"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-win32.whl", hash = "sha256:c1e8edc49b32483cd5d2d015f343e16be7dfab89f4aaf66b0fa6827ab356880d"}, + {file = "SQLAlchemy-2.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:77a380bf8721b416782c763e0ff66f80f3b05aee83db33ddfc0eac20bcb6791f"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a2f9120eb32190bdba31d1022181ef08f257aed4f984f3368aa4e838de72bc0"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:679b9bd10bb32b8d3befed4aad4356799b6ec1bdddc0f930a79e41ba5b084124"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:582053571125895d008d4b8d9687d12d4bd209c076cdbab3504da307e2a0a2bd"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c82395e2925639e6d320592943608070678e7157bd1db2672a63be9c7889434"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:25e4e54575f9d2af1eab82d3a470fca27062191c48ee57b6386fe09a3c0a6a33"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9946ee503962859f1a9e1ad17dff0859269b0cb453686747fe87f00b0e030b34"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-win32.whl", hash = "sha256:c621f05859caed5c0aab032888a3d3bde2cae3988ca151113cbecf262adad976"}, + {file = "SQLAlchemy-2.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:662a79e80f3e9fe33b7861c19fedf3d8389fab2413c04bba787e3f1139c22188"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3f927340b37fe65ec42e19af7ce15260a73e11c6b456febb59009bfdfec29a35"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67901b91bf5821482fcbe9da988cb16897809624ddf0fde339cd62365cc50032"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1644c603558590f465b3fa16e4557d87d3962bc2c81fd7ea85b582ecf4676b31"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9a7ecaf90fe9ec8e45c86828f4f183564b33c9514e08667ca59e526fea63893a"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8a88b32ce5b69d18507ffc9f10401833934ebc353c7b30d1e056023c64f0a736"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-win32.whl", hash = "sha256:2267c004e78e291bba0dc766a9711c389649cf3e662cd46eec2bc2c238c637bd"}, + {file = "SQLAlchemy-2.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:59cf0cdb29baec4e074c7520d7226646a8a8f856b87d8300f3e4494901d55235"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd801375f19a6e1f021dabd8b1714f2fdb91cbc835cd13b5dd0bd7e9860392d7"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8efdda920988bcade542f53a2890751ff680474d548f32df919a35a21404e3f"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:918c2b553e3c78268b187f70983c9bc6f91e451a4f934827e9c919e03d258bd7"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d05773d5c79f2d3371d81697d54ee1b2c32085ad434ce9de4482e457ecb018"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fdb2686eb01f670cdc6c43f092e333ff08c1cf0b646da5256c1237dc4ceef4ae"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ff0a7c669ec7cdb899eae7e622211c2dd8725b82655db2b41740d39e3cda466"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-win32.whl", hash = "sha256:57dcd9eed52413f7270b22797aa83c71b698db153d1541c1e83d45ecdf8e95e7"}, + {file = "SQLAlchemy-2.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:54aa9f40d88728dd058e951eeb5ecc55241831ba4011e60c641738c1da0146b7"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:817aab80f7e8fe581696dae7aaeb2ceb0b7ea70ad03c95483c9115970d2a9b00"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc7b9f55c2f72c13b2328b8a870ff585c993ba1b5c155ece5c9d3216fa4b18f6"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f696828784ab2c07b127bfd2f2d513f47ec58924c29cff5b19806ac37acee31c"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce54965a94673a0ebda25e7c3a05bf1aa74fd78cc452a1a710b704bf73fb8402"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f342057422d6bcfdd4996e34cd5c7f78f7e500112f64b113f334cdfc6a0c593d"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5deafb4901618b3f98e8df7099cd11edd0d1e6856912647e28968b803de0dae"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-win32.whl", hash = "sha256:81f1ea264278fcbe113b9a5840f13a356cb0186e55b52168334124f1cd1bc495"}, + {file = "SQLAlchemy-2.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:954f1ad73b78ea5ba5a35c89c4a5dfd0f3a06c17926503de19510eb9b3857bde"}, + {file = "SQLAlchemy-2.0.4-py3-none-any.whl", hash = "sha256:0adca8a3ca77234a142c5afed29322fb501921f13d1d5e9fa4253450d786c160"}, + {file = "SQLAlchemy-2.0.4.tar.gz", hash = "sha256:95a18e1a6af2114dbd9ee4f168ad33070d6317e11bafa28d983cc7b585fe900b"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.2.0" + +[package.extras] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=7)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + [[package]] name = "werkzeug" version = "2.2.3" @@ -508,4 +672,4 @@ watchdog = ["watchdog"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "850ee074cc1e4ad92a8650334d306e3c4269062f0324887a2d6f19149f4d5a30" +content-hash = "8577c3b41be81184b268f983c0958e58169f3df0c179b296f3d4be40e0865737" diff --git a/pyproject.toml b/pyproject.toml index 7b57875..fbf73ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ libsass = "^0.22.0" colorthief = "^0.2.1" Pillow = "^9.4.0" platformdirs = "^3.0.0" +SQLAlchemy = "^2.0.3" [build-system]