Switch to Flask-SQLAlchemy

Add FLask-Migrate for next step in the Migration 😉
This commit is contained in:
Michał 2023-04-09 19:12:35 +00:00
parent 7d0078ea9a
commit 7c553e99b8
12 changed files with 368 additions and 403 deletions

View file

@ -2,41 +2,31 @@
Onlylegs Gallery
This is the main app file, it loads all the other files and sets up the app
"""
# Import system modules
import os
import logging
# Flask
from flask_compress import Compress
from flask_caching import Cache
from flask_assets import Environment, Bundle
from flask_login import LoginManager
from flask_assets import Bundle
from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
from gallery.extensions import db, migrate, login_manager, assets, compress, cache
from gallery.models import Users
from gallery.views import index, image, group, settings, profile
from gallery import api
from gallery import auth
# Configuration
import platformdirs
from dotenv import load_dotenv
from yaml import safe_load
# Import database
from sqlalchemy.orm import sessionmaker
from gallery import db
USER_DIR = platformdirs.user_config_dir("onlylegs")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
login_manager = LoginManager()
assets = Environment()
cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300})
compress = Compress()
def create_app(test_config=None): # pylint: disable=R0914
def create_app(): # pylint: disable=R0914
"""
Create and configure the main app
"""
@ -54,7 +44,7 @@ def create_app(test_config=None): # pylint: disable=R0914
# App configuration
app.config.from_mapping(
SECRET_KEY=os.environ.get("FLASK_SECRET"),
DATABASE=os.path.join(app.instance_path, "gallery.sqlite3"),
SQLALCHEMY_DATABASE_URI=("sqlite:///gallery.sqlite3"),
UPLOAD_FOLDER=os.path.join(USER_DIR, "uploads"),
ALLOWED_EXTENSIONS=conf["upload"]["allowed-extensions"],
MAX_CONTENT_LENGTH=1024 * 1024 * conf["upload"]["max-size"],
@ -63,10 +53,8 @@ def create_app(test_config=None): # pylint: disable=R0914
WEBSITE_CONF=conf["website"],
)
if test_config is None:
app.config.from_pyfile("config.py", silent=True)
else:
app.config.from_mapping(test_config)
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
login_manager.login_view = "gallery.index"
@ -74,7 +62,7 @@ def create_app(test_config=None): # pylint: disable=R0914
@login_manager.user_loader
def load_user(user_id):
return db_session.query(db.Users).filter_by(alt_id=user_id).first()
return Users.query.filter_by(alt_id=user_id).first()
@login_manager.unauthorized_handler
def unauthorized():
@ -82,23 +70,6 @@ def create_app(test_config=None): # pylint: disable=R0914
msg = "You are not authorized to view this page!!!!"
return render_template("error.html", error=error, msg=msg), error
scripts = Bundle(
"js/*.js",
filters="jsmin",
output="gen/js.js",
depends="js/*.js"
)
styles = Bundle(
"sass/*.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
)
assets.register("scripts", scripts)
assets.register("styles", styles)
# Error handlers, if the error is not a HTTP error, return 500
@app.errorhandler(Exception)
def error_page(err): # noqa
@ -109,30 +80,30 @@ def create_app(test_config=None): # pylint: disable=R0914
err.code,
)
# Load login, registration and logout manager
from gallery import auth
scripts = Bundle("js/*.js", filters="jsmin", output="gen/js.js", depends="js/*.js")
styles = Bundle(
"sass/*.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
)
assets.register("scripts", scripts)
assets.register("styles", styles)
# Load all the blueprints
app.register_blueprint(auth.blueprint)
# Load the API
from gallery import api
app.register_blueprint(api.blueprint)
# Load the different views
from gallery.views import index, image, group, settings, profile
app.register_blueprint(index.blueprint)
app.register_blueprint(image.blueprint)
app.register_blueprint(group.blueprint)
app.register_blueprint(profile.blueprint)
app.register_blueprint(settings.blueprint)
# Log to file that the app has started
logging.info("Gallery started successfully!")
# Initialize extensions and return app
assets.init_app(app)
cache.init_app(app)
compress.init_app(app)
logging.info("Gallery started successfully!")
return app

View file

@ -14,16 +14,14 @@ from flask_login import login_required, current_user
from colorthief import ColorThief
from sqlalchemy.orm import sessionmaker
from gallery.extensions import db
from gallery.models import Posts, Groups, GroupJunction
from gallery import db
from gallery.utils import metadata as mt
from gallery.utils.generate_image import generate_thumbnail
blueprint = Blueprint("api", __name__, url_prefix="/api")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/file/<file_name>", methods=["GET"])
@ -87,7 +85,7 @@ def upload():
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database
query = db.Posts(
query = Posts(
author_id=current_user.id,
filename=img_name + "." + img_ext,
mimetype=img_ext,
@ -97,8 +95,8 @@ def upload():
alt=form["alt"],
)
db_session.add(query)
db_session.commit()
db.session.add(query)
db.session.commit()
return "Gwa Gwa" # Return something so the browser doesn't show an error
@ -109,7 +107,7 @@ def delete_image(image_id):
"""
Deletes an image from the server and database
"""
img = db_session.query(db.Posts).filter_by(id=image_id).first()
img = Posts.query.filter_by(id=image_id).first()
# Check if image exists and if user is allowed to delete it (author)
if img is None:
@ -131,16 +129,15 @@ def delete_image(image_id):
for cache_file in pathlib.Path(cache_path).glob(cache_name + "*"):
os.remove(cache_file)
# Delete from database
db_session.query(db.Posts).filter_by(id=image_id).delete()
# Remove all entries in junction table
groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all()
post = Posts.query.filter_by(id=image_id).first()
db.session.delete(post)
groups = GroupJunction.query.filter_by(post_id=image_id).all()
for group in groups:
db_session.delete(group)
db.session.delete(group)
# Commit all changes
db_session.commit()
db.session.commit()
logging.info("Removed image (%s) %s", image_id, img.filename)
flash(["Image was all in Le Head!", "1"])
@ -153,14 +150,14 @@ def create_group():
"""
Creates a group
"""
new_group = db.Groups(
new_group = Groups(
name=request.form["name"],
description=request.form["description"],
author_id=current_user.id,
)
db_session.add(new_group)
db_session.commit()
db.session.add(new_group)
db.session.commit()
return ":3"
@ -175,7 +172,7 @@ def modify_group():
image_id = request.form["image"]
action = request.form["action"]
group = db_session.query(db.Groups).filter_by(id=group_id).first()
group = Groups.query.filter_by(id=group_id).first()
if group is None:
abort(404)
@ -183,20 +180,12 @@ def modify_group():
abort(403)
if action == "add":
if not (
db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id)
.first()
):
db_session.add(db.GroupJunction(group_id=group_id, post_id=image_id))
if not GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).first():
db.session.add(GroupJunction(group_id=group_id, post_id=image_id))
elif request.form["action"] == "remove":
(
db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id)
.delete()
)
db.session.delete(GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).first())
db_session.commit()
db.session.commit()
return ":3"
@ -208,16 +197,21 @@ def delete_group():
"""
group_id = request.form["group"]
group = db_session.query(db.Groups).filter_by(id=group_id).first()
group = Groups.query.filter_by(id=group_id).first()
if group is None:
abort(404)
elif group.author_id != current_user.id:
abort(403)
db_session.query(db.Groups).filter_by(id=group_id).delete()
db_session.query(db.GroupJunction).filter_by(group_id=group_id).delete()
db_session.commit()
group_del = Groups.query.filter_by(id=group_id).first()
db.session.delete(group_del)
junction_del = GroupJunction.query.filter_by(group_id=group_id).all()
for junction in junction_del:
db.session.delete(junction)
db.session.commit()
flash(["Group yeeted!", "1"])
return ":3"

View file

@ -10,13 +10,11 @@ from werkzeug.security import check_password_hash, generate_password_hash
from flask_login import login_user, logout_user, login_required
from sqlalchemy.orm import sessionmaker
from gallery import db
from gallery.extensions import db
from gallery.models import Users
blueprint = Blueprint("auth", __name__, url_prefix="/auth")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/login", methods=["POST"])
@ -30,7 +28,7 @@ def login():
password = request.form["password"].strip()
remember = bool(request.form["remember-me"])
user = db_session.query(db.Users).filter_by(username=username).first()
user = Users.query.filter_by(username=username).first()
if not user or not check_password_hash(user.password, password):
logging.error("Login attempt from %s", request.remote_addr)
@ -79,7 +77,7 @@ def register():
elif password_repeat != password:
error.append("Passwords do not match!")
user_exists = db_session.query(db.Users).filter_by(username=username).first()
user_exists = Users.query.filter_by(username=username).first()
if user_exists:
error.append("User already exists!")
@ -93,8 +91,8 @@ def register():
email=email,
password=generate_password_hash(password, method="sha256"),
)
db_session.add(register_user)
db_session.commit()
db.session.add(register_user)
db.session.commit()
logging.info("User %s registered", username)
return "ok", 200

View file

@ -1,153 +0,0 @@
"""
OnlyLegs - Database models and ions for SQLAlchemy
"""
from uuid import uuid4
import os
import platformdirs
from sqlalchemy import (
create_engine,
Column,
Integer,
String,
DateTime,
ForeignKey,
PickleType,
func,
)
from sqlalchemy.orm import declarative_base, relationship
from flask_login import UserMixin
USER_DIR = platformdirs.user_config_dir("onlylegs")
DB_PATH = os.path.join(USER_DIR, "instance", "gallery.sqlite3")
# In the future, I want to add support for other databases
engine = create_engine(f"sqlite:///{DB_PATH}", echo=False)
base = declarative_base()
class Users(base, UserMixin): # pylint: disable=too-few-public-methods, C0103
"""
User table
Joins with post, groups, session and log
"""
__tablename__ = "users"
# Gallery used information
id = Column(Integer, primary_key=True)
alt_id = Column(String, unique=True, nullable=False, default=str(uuid4()))
profile_picture = Column(String, nullable=True, default=None)
username = Column(String, unique=True, nullable=False)
email = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False)
joined_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
posts = relationship("Posts", backref="users")
groups = relationship("Groups", backref="users")
log = relationship("Logs", backref="users")
def get_id(self):
return str(self.alt_id)
class Posts(base): # pylint: disable=too-few-public-methods, C0103
"""
Post table
Joins with group_junction
"""
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
filename = Column(String, unique=True, nullable=False)
mimetype = Column(String, nullable=False)
exif = Column(PickleType, nullable=False)
colours = Column(PickleType, nullable=False)
description = Column(String, nullable=False)
alt = Column(String, nullable=False)
junction = relationship("GroupJunction", backref="posts")
class Groups(base): # pylint: disable=too-few-public-methods, C0103
"""
Group table
Joins with group_junction
"""
__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, server_default=func.now() # pylint: disable=E1102
)
junction = relationship("GroupJunction", backref="groups")
class GroupJunction(base): # pylint: disable=too-few-public-methods, C0103
"""
Junction table for posts and groups
Joins with posts and groups
"""
__tablename__ = "group_junction"
id = Column(Integer, primary_key=True)
date_added = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
group_id = Column(Integer, ForeignKey("groups.id"))
post_id = Column(Integer, ForeignKey("posts.id"))
class Logs(base): # pylint: disable=too-few-public-methods, C0103
"""
Log table
Joins with user
"""
__tablename__ = "logs"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False)
note = Column(String, nullable=False)
created_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
class Bans(base): # pylint: disable=too-few-public-methods, C0103
"""
Bans table
"""
__tablename__ = "bans"
id = Column(Integer, primary_key=True)
ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False)
note = Column(String, nullable=False)
banned_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
# check if database file exists, if not create it
if not os.path.isfile(DB_PATH):
base.metadata.create_all(engine)
print("Database created")

13
gallery/extensions.py Normal file
View file

@ -0,0 +1,13 @@
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_assets import Environment
from flask_compress import Compress
from flask_caching import Cache
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
assets = Environment()
compress = Compress()
cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300})

138
gallery/models.py Normal file
View file

@ -0,0 +1,138 @@
"""
OnlyLegs - Database models and ions for SQLAlchemy
"""
from uuid import uuid4
from flask_login import UserMixin
from .extensions import db
class Users(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103
"""
User table
Joins with post, groups, session and log
"""
__tablename__ = "users"
# Gallery used information
id = db.Column(db.Integer, primary_key=True)
alt_id = db.Column(db.String, unique=True, nullable=False, default=str(uuid4()))
profile_picture = db.Column(db.String, nullable=True, default=None)
username = db.Column(db.String, unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
joined_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(), # pylint: disable=E1102
)
posts = db.relationship("Posts", backref="users")
groups = db.relationship("Groups", backref="users")
log = db.relationship("Logs", backref="users")
def get_id(self):
return str(self.alt_id)
class Posts(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Post table
Joins with group_junction
"""
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(), # pylint: disable=E1102
)
filename = db.Column(db.String, unique=True, nullable=False)
mimetype = db.Column(db.String, nullable=False)
exif = db.Column(db.PickleType, nullable=False)
colours = db.Column(db.PickleType, nullable=False)
description = db.Column(db.String, nullable=False)
alt = db.Column(db.String, nullable=False)
junction = db.relationship("GroupJunction", backref="posts")
class Groups(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Group table
Joins with group_junction
"""
__tablename__ = "groups"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
description = db.Column(db.String, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(), # pylint: disable=E1102
)
junction = db.relationship("GroupJunction", backref="groups")
class GroupJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Junction table for posts and groups
Joins with posts and groups
"""
__tablename__ = "group_junction"
id = db.Column(db.Integer, primary_key=True)
date_added = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(), # pylint: disable=E1102
)
group_id = db.Column(db.Integer, db.ForeignKey("groups.id"))
post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
class Logs(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Log table
Joins with user
"""
__tablename__ = "logs"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
ip_address = db.Column(db.String, nullable=False)
code = db.Column(db.Integer, nullable=False)
note = db.Column(db.String, nullable=False)
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(), # pylint: disable=E1102
)
class Bans(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Bans table
"""
__tablename__ = "bans"
id = db.Column(db.Integer, primary_key=True)
ip_address = db.Column(db.String, nullable=False)
code = db.Column(db.Integer, nullable=False)
note = db.Column(db.String, nullable=False)
banned_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(), # pylint: disable=E1102
)

View file

@ -5,14 +5,11 @@ sounds more limiting that it actually is in this gallery
"""
from flask import Blueprint, abort, render_template, url_for
from sqlalchemy.orm import sessionmaker
from gallery import db
from gallery.models import Posts, Users, GroupJunction, Groups
from gallery.utils import contrast
blueprint = Blueprint("group", __name__, url_prefix="/group")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/", methods=["GET"])
@ -20,21 +17,21 @@ def groups():
"""
Group overview, shows all image groups
"""
groups = db_session.query(db.Groups).all()
groups = Groups.query.all()
# For each group, get the 3 most recent images
for group in groups:
group.author_username = (
db_session.query(db.Users.username)
.filter(db.Users.id == group.author_id)
Users.query.with_entities(Users.username)
.filter(Users.id == group.author_id)
.first()[0]
)
# Get the 3 most recent images
images = (
db_session.query(db.GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group.id)
.order_by(db.GroupJunction.date_added.desc())
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group.id)
.order_by(GroupJunction.date_added.desc())
.limit(3)
)
@ -42,10 +39,10 @@ def groups():
group.images = []
for image in images:
group.images.append(
db_session.query(
db.Posts.filename, db.Posts.alt, db.Posts.colours, db.Posts.id
Posts.query.with_entities(
Posts.filename, Posts.alt, Posts.colours, Posts.id
)
.filter(db.Posts.id == image[0])
.filter(Posts.id == image[0])
.first()
)
@ -58,32 +55,30 @@ def group(group_id):
Group view, shows all images in a group
"""
# Get the group, if it doesn't exist, 404
group = db_session.query(db.Groups).filter(db.Groups.id == group_id).first()
group = Groups.query.filter(Groups.id == group_id).first()
if group is None:
abort(404, "Group not found! D:")
# Get the group's author username
group.author_username = (
db_session.query(db.Users.username)
.filter(db.Users.id == group.author_id)
Users.query.with_entities(Users.username)
.filter(Users.id == group.author_id)
.first()[0]
)
# Get all images in the group from the junction table
junction = (
db_session.query(db.GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group_id)
.order_by(db.GroupJunction.date_added.desc())
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group_id)
.order_by(GroupJunction.date_added.desc())
.all()
)
# Get the image data for each image in the group
images = []
for image in junction:
images.append(
db_session.query(db.Posts).filter(db.Posts.id == image[0]).first()
)
images.append(Posts.query.filter(Posts.id == image[0]).first())
# Check contrast for the first image in the group for the banner
text_colour = "rgb(var(--fg-black))"
@ -103,21 +98,21 @@ def group_post(group_id, image_id):
Image view, shows the image and its metadata from a specific group
"""
# Get the image, if it doesn't exist, 404
image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first()
image = Posts.query.filter(Posts.id == image_id).first()
if image is None:
abort(404, "Image not found")
# Get the image's author username
image.author_username = (
db_session.query(db.Users.username)
.filter(db.Users.id == image.author_id)
Users.query.with_entities(Users.username)
.filter(Users.id == image.author_id)
.first()[0]
)
# Get all groups the image is in
groups = (
db_session.query(db.GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id)
GroupJunction.query.with_entities(GroupJunction.group_id)
.filter(GroupJunction.post_id == image_id)
.all()
)
@ -125,24 +120,24 @@ def group_post(group_id, image_id):
image.groups = []
for group in groups:
image.groups.append(
db_session.query(db.Groups.id, db.Groups.name)
.filter(db.Groups.id == group[0])
Groups.query.with_entities(Groups.id, Groups.name)
.filter(Groups.id == group[0])
.first()
)
# Get the next and previous images in the group
next_url = (
db_session.query(db.GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group_id)
.filter(db.GroupJunction.post_id > image_id)
.order_by(db.GroupJunction.date_added.asc())
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group_id)
.filter(GroupJunction.post_id > image_id)
.order_by(GroupJunction.date_added.asc())
.first()
)
prev_url = (
db_session.query(db.GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group_id)
.filter(db.GroupJunction.post_id < image_id)
.order_by(db.GroupJunction.date_added.desc())
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group_id)
.filter(GroupJunction.post_id < image_id)
.order_by(GroupJunction.date_added.desc())
.first()
)

View file

@ -5,13 +5,10 @@ from math import ceil
from flask import Blueprint, abort, render_template, url_for, current_app
from sqlalchemy.orm import sessionmaker
from gallery import db
from gallery.models import Posts, Users, GroupJunction, Groups
blueprint = Blueprint("image", __name__, url_prefix="/image")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/<int:image_id>")
@ -20,21 +17,21 @@ def image(image_id):
Image view, shows the image and its metadata
"""
# Get the image, if it doesn't exist, 404
image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first()
image = Posts.query.filter(Posts.id == image_id).first()
if not image:
abort(404, "Image not found :<")
# Get the image's author username
image.author_username = (
db_session.query(db.Users.username)
.filter(db.Users.id == image.author_id)
Users.query.with_entities(Users.username)
.filter(Users.id == image.author_id)
.first()[0]
)
# Get the image's groups
groups = (
db_session.query(db.GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id)
GroupJunction.query.with_entities(GroupJunction.group_id)
.filter(GroupJunction.post_id == image_id)
.all()
)
@ -42,23 +39,23 @@ def image(image_id):
image.groups = []
for group in groups:
image.groups.append(
db_session.query(db.Groups.id, db.Groups.name)
.filter(db.Groups.id == group[0])
Groups.query.with_entities(Groups.name, Groups.id)
.filter(Groups.id == group[0])
.first()
)
# Get the next and previous images
# Check if there is a group ID set
next_url = (
db_session.query(db.Posts.id)
.filter(db.Posts.id > image_id)
.order_by(db.Posts.id.asc())
Posts.query.with_entities(Posts.id)
.filter(Posts.id > image_id)
.order_by(Posts.id.asc())
.first()
)
prev_url = (
db_session.query(db.Posts.id)
.filter(db.Posts.id < image_id)
.order_by(db.Posts.id.desc())
Posts.query.with_entities(Posts.id)
.filter(Posts.id < image_id)
.order_by(Posts.id.desc())
.first()
)
@ -69,7 +66,7 @@ def image(image_id):
prev_url = url_for("image.image", image_id=prev_url[0])
# Yoink all the images in the database
total_images = db_session.query(db.Posts.id).order_by(db.Posts.id.desc()).all()
total_images = Posts.query.with_entities(Posts.id).order_by(Posts.id.desc()).all()
limit = current_app.config["UPLOAD_CONF"]["max-load"]
# If the number of items is less than the limit, no point of calculating the page

View file

@ -6,13 +6,10 @@ from math import ceil
from flask import Blueprint, render_template, request, current_app
from werkzeug.exceptions import abort
from sqlalchemy.orm import sessionmaker
from gallery import db
from gallery.models import Posts
blueprint = Blueprint("gallery", __name__)
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/")
@ -30,7 +27,7 @@ def index():
# get the total number of images in the database
# calculate the total number of pages, and make sure the page number is valid
total_images = db_session.query(db.Posts.id).count()
total_images = Posts.query.with_entities(Posts.id).count()
pages = ceil(max(total_images, limit) / limit)
if page > pages:
abort(
@ -41,14 +38,10 @@ def index():
# get the images for the current page
images = (
db_session.query(
db.Posts.filename,
db.Posts.alt,
db.Posts.colours,
db.Posts.created_at,
db.Posts.id,
Posts.query.with_entities(
Posts.filename, Posts.alt, Posts.colours, Posts.created_at, Posts.id
)
.order_by(db.Posts.id.desc())
.order_by(Posts.id.desc())
.offset((page - 1) * limit)
.limit(limit)
.all()

View file

@ -5,13 +5,10 @@ from flask import Blueprint, render_template, request
from werkzeug.exceptions import abort
from flask_login import current_user
from sqlalchemy.orm import sessionmaker
from gallery import db
from gallery.models import Posts, Users
blueprint = Blueprint("profile", __name__, url_prefix="/profile")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/profile")
@ -29,11 +26,11 @@ def profile():
abort(404, "You must be logged in to view your own profile!")
# Get the user's data
user = db_session.query(db.Users).filter(db.Users.id == user_id).first()
user = Users.query.filter(Users.id == user_id).first()
if not user:
abort(404, "User not found :c")
images = db_session.query(db.Posts).filter(db.Posts.author_id == user_id).all()
images = Posts.query.filter(Posts.author_id == user_id).all()
return render_template("profile.html", user=user, images=images)

175
poetry.lock generated
View file

@ -1,5 +1,27 @@
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
[[package]]
name = "alembic"
version = "1.10.3"
description = "A database migration tool for SQLAlchemy."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"},
{file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"},
]
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.9\""}
importlib-resources = {version = "*", markers = "python_version < \"3.9\""}
Mako = "*"
SQLAlchemy = ">=1.3.0"
typing-extensions = ">=4"
[package.extras]
tz = ["python-dateutil"]
[[package]]
name = "astroid"
version = "2.15.2"
@ -192,79 +214,6 @@ files = [
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "dukpy"
version = "0.3.0"
description = "Simple JavaScript interpreter for Python"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "dukpy-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47ed8813baf52ad3e3a7d4c7416173af0693bbfab1f3b685cbf0165e0e376769"},
{file = "dukpy-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4b3a69977d89c83d74e64a5feb7264acb007c251e2eb83bc4e79c818b73b4fc"},
{file = "dukpy-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:782e60979db86f7ae9d5e84185cf6c252954cbcfda982353dd30ff6a17fef0be"},
{file = "dukpy-0.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3698f35c184b3319257d4d7bfa796ef109e8f78fc3cef8e22a3bf0f2d0eef774"},
{file = "dukpy-0.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bd7f6ded168548d808e3a3ac97ccf98ee1a97c327e7e67c13229932f3c923f85"},
{file = "dukpy-0.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9e044f3e78881f3c1fc0b939349551a9be2e2519d4e670038ce497d7cc780c69"},
{file = "dukpy-0.3.0-cp310-cp310-win32.whl", hash = "sha256:c1827f1f7282bb0cc329c7f687c0f58d87f5736777e553f483c26636e9bd1960"},
{file = "dukpy-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d9697701eb1a01c0044479b3fa501685adc1a699ffe1acbb39b0b724bc1a7bac"},
{file = "dukpy-0.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c189ae4b5c5deb2b576cd0b0ae0193dbd7e15a1499491b3798f3ed7aae8274b1"},
{file = "dukpy-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:531db11c50326c1baa00711a8221995ec0935418c690d02a84ef9ce537968686"},
{file = "dukpy-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c072d28ff58db698eb5bfa4556f59e5ce4d4f219b176c93375bfda87c117253f"},
{file = "dukpy-0.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8171990e640625ad5876a0548072220ebd34c9f0705510144082ce34a2e777b"},
{file = "dukpy-0.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:74e0a194e8908bfa64ea2e2e353cf28184d498ed675174a96d948ac2dd6db24e"},
{file = "dukpy-0.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0aebd4be1109e58126ff4e959415f3198390b92dc48cf6144b97caeb786cf0df"},
{file = "dukpy-0.3.0-cp311-cp311-win32.whl", hash = "sha256:ff24928cf9c14af226cf575640e2166611a79d8fd14ea498183ca7cd7ab349e5"},
{file = "dukpy-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:a96a600ce653c5fb9c7190af8c1e82b7d212709dfdd31ce65a2e328cbd923dd6"},
{file = "dukpy-0.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:581bbd180a7d69149a1b3171d987a8d1eedf988ce3d138ca2e1730888012e41a"},
{file = "dukpy-0.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0636f4658024033427907b3a67b9bbc9c405fd7ee1f924ec1b1eca070d7a6efb"},
{file = "dukpy-0.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1628e9171e900d2b97e45a22709e68f91bb88ef8dbabfc0c1f4f92524eeb900e"},
{file = "dukpy-0.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:2c1576a480656ee4bce9bb2f471623b894c8ab809617bdf08b8f547a990df063"},
{file = "dukpy-0.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:87202891b5dd85053321b561173ebbe84ceab58f9cd4e6c028686e5793bfc976"},
{file = "dukpy-0.3.0-cp36-cp36m-win32.whl", hash = "sha256:6b2ef5b42a666d4cd73618dce1b9b182c02f15cf52598aef4047e0ecbed2f4ed"},
{file = "dukpy-0.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6e16d07a506e79af132a7d1b4d28b7846d1e980a8a965130bfe755f56922f35e"},
{file = "dukpy-0.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7708973d15bc01c91e68195338f9db0a6d4b1e663e2a778da2db00b8c27e7488"},
{file = "dukpy-0.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:218c26430b424cd2fa4a8a0e252acf835719ee2107937d01c7bbc15615b07e0d"},
{file = "dukpy-0.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d1f25e485a77e1318b95db43717454001e412adec0ba268dfc8eecf3b893d45"},
{file = "dukpy-0.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5a1614b73884c14a00b496384d2e793bfd07dfcac425eb1fe768e5b870118111"},
{file = "dukpy-0.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1005579d0e3fd7ab35e18138da3172baa59e3305f22a55fbe9961c67204b1ddd"},
{file = "dukpy-0.3.0-cp37-cp37m-win32.whl", hash = "sha256:e59a93c819cb818251e7d8ad0b548163227fec3b8485c4cdcecfac59abd9db87"},
{file = "dukpy-0.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d10c0cd5035e3e2dc27d193734537546f1910d2dc0ccd468bb510924313bbaa2"},
{file = "dukpy-0.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3bee97f928e0477a197fcc66f25a8d46d1ebc7068ddda2f657445cced303111b"},
{file = "dukpy-0.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bd7e8b90590122b92e8057052e485afdcc4a6145e50036cc55deac045dd6568f"},
{file = "dukpy-0.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2feb5c2d05b3c9b8fafc9088c5c025a14c9e239f96abb1aa75ebc022f1777e9c"},
{file = "dukpy-0.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09ae9309490fc578da96611fc50e46f02c32616e53f55f2bc9c864f67e6c759e"},
{file = "dukpy-0.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf7412d2d6883fe0ff498cbdb0e67e16804972cf216c169d83aa8d5bad50d109"},
{file = "dukpy-0.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a8e06c2402031e030924088b37bbde27cf43936bf8ff0ff65c9bdfd9bf4ae89c"},
{file = "dukpy-0.3.0-cp38-cp38-win32.whl", hash = "sha256:d87b932a387d4015d9acdb99b94c788453b19b5aa5fd10584098e042d8c7118a"},
{file = "dukpy-0.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e31213f8cbbf85d0386f0ea0e478cd0e4dd918a8747d568a6936044dbd21330c"},
{file = "dukpy-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49f6390bbc47b618fdb19d7af89e73f643f308a2ab9f5d5e0eb161d4508f23c6"},
{file = "dukpy-0.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a95ff658b7400e71acaab453359ea74d1a1625cebb937d0294a053b6aac3e507"},
{file = "dukpy-0.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9087a3321649beb17f91afa6ffde991d477aa0029c3be5ce908369517ac85251"},
{file = "dukpy-0.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6acc3a3ce997aef380f79f1985636d87701c1841707c0748ee5eff65e396f0b2"},
{file = "dukpy-0.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1329be71ce19fdda899a0b59cd531b711adc0d30867488f7401b38b518415a9"},
{file = "dukpy-0.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0c6ebf8e495f9750f2820cdedfe384621369ebef562ed52770d7a9f070e5991e"},
{file = "dukpy-0.3.0-cp39-cp39-win32.whl", hash = "sha256:6ae877b9d439941e2afcaaaa410ef168c51e885f99665bf591b97a71eafaeb0f"},
{file = "dukpy-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1a8df866eb0af6b55f1a27786f5217334a4e904fd04b7c285c4ee5b684072abe"},
{file = "dukpy-0.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b895adaab9feaec6e33ba221bcfc16bd50710b18346077b8cec06e843355fb6"},
{file = "dukpy-0.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2edf126a5c8da0b1ffc39381323d3129cf922d041c74c78402652c9efdf74c99"},
{file = "dukpy-0.3.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f0f517d245b69781ad91dcb6d9d1a9550b2dbb0d8b636b9e8899838780ad211"},
{file = "dukpy-0.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:99f76adf6f9c40b0501d7fffc1570a7b7dc4eaf8b2d3cb38ac738068ba2731e6"},
{file = "dukpy-0.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:eca56334b67370427c503b65f21424d317b7560620e28809b4852828a9fcea54"},
{file = "dukpy-0.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3da5a1bc3ce7788ed05cc16fc67f9be5e187ed4f6fedcf1fd6574633a5230be"},
{file = "dukpy-0.3.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cb574f3b71545adbccbb7688059b1a63eca057c59ac00004a6de196eb95844a"},
{file = "dukpy-0.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b4b068796a4d81c37673e9d949a6307dbadc2cd6c2062b6010bd6561a24895fb"},
{file = "dukpy-0.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:87a9ea4cb2593220e0c6abc6a0b5849e940de78c1e464fe6a4339efe655fd3af"},
{file = "dukpy-0.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1185f27c920889a41e189ba8a2c76211ee84be1ea1bb4c1f7cc4343f9a1a3d2c"},
{file = "dukpy-0.3.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a60d5b3537800944cb6e8bedbf68a724dea92a6f9a8ce9a48530838e68478716"},
{file = "dukpy-0.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:157280c79833f223f3dc6effe8981525e68cd262e26947b2cddd57addac9a3d8"},
{file = "dukpy-0.3.0.tar.gz", hash = "sha256:ca5772e9373f3cf7772a711e65db765c4361dcf6d4e65c5d88cb879e9ee3f5a6"},
]
[package.extras]
testing = ["mock", "pytest", "pytest-cov"]
webassets = ["webassets"]
[[package]]
name = "flask"
version = "2.2.3"
@ -352,6 +301,39 @@ files = [
Flask = ">=1.0.4"
Werkzeug = ">=1.0.1"
[[package]]
name = "flask-migrate"
version = "4.0.4"
description = "SQLAlchemy database migrations for Flask applications using Alembic."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"},
{file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"},
]
[package.dependencies]
alembic = ">=1.9.0"
Flask = ">=0.9"
Flask-SQLAlchemy = ">=1.0"
[[package]]
name = "flask-sqlalchemy"
version = "3.0.3"
description = "Add SQLAlchemy support to your Flask application."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"},
{file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"},
]
[package.dependencies]
Flask = ">=2.2"
SQLAlchemy = ">=1.4.18"
[[package]]
name = "greenlet"
version = "2.0.2"
@ -449,14 +431,14 @@ tornado = ["tornado (>=0.2)"]
[[package]]
name = "importlib-metadata"
version = "6.2.0"
version = "6.2.1"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_metadata-6.2.0-py3-none-any.whl", hash = "sha256:8388b74023a138c605fddd0d47cb81dd706232569f56c9aca7d9c7fdb54caeba"},
{file = "importlib_metadata-6.2.0.tar.gz", hash = "sha256:9127aad2f49d7203e7112098c12b92e4fd1061ccd18548cdfdc49171a8c073cc"},
{file = "importlib_metadata-6.2.1-py3-none-any.whl", hash = "sha256:f65e478a7c2177bd19517a3a15dac094d253446d8690c5f3e71e735a04312374"},
{file = "importlib_metadata-6.2.1.tar.gz", hash = "sha256:5a66966b39ff1c14ef5b2d60c1d842b0141fefff0f4cc6365b4bc9446c652807"},
]
[package.dependencies]
@ -467,6 +449,25 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker
perf = ["ipython"]
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "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 (>=0.9.2)"]
[[package]]
name = "importlib-resources"
version = "5.12.0"
description = "Read resources from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"},
{file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"},
]
[package.dependencies]
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[[package]]
name = "isort"
version = "5.12.0"
@ -587,6 +588,26 @@ files = [
{file = "libsass-0.22.0.tar.gz", hash = "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425"},
]
[[package]]
name = "mako"
version = "1.2.4"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
{file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
]
[package.dependencies]
MarkupSafe = ">=0.9.2"
[package.extras]
babel = ["Babel"]
lingua = ["lingua"]
testing = ["pytest"]
[[package]]
name = "markupsafe"
version = "2.1.2"
@ -1115,4 +1136,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "8ff06b3873e6466deca84a8258f87f8aa250072d02e30564fbd554e826754e61"
content-hash = "88387c49c901feebd4685ee75f6b79c0bbc8cc9c1a64798cd0394b140e165603"

View file

@ -9,19 +9,20 @@ readme = ".github/README.md"
[tool.poetry.dependencies]
python = "^3.8"
Flask = "^2.2.2"
Flask-Sqlalchemy = "^3.0.3"
Flask-Migrate = "^4.0.4"
Flask-Compress = "^1.13"
Flask-Caching = "^2.0.2"
Flask-Assets = "^2.0"
Flask-Login = "^0.6.2"
SQLAlchemy = "^2.0.3"
python-dotenv = "^0.21.0"
gunicorn = "^20.1.0"
pyyaml = "^6.0"
libsass = "^0.22.0"
colorthief = "^0.2.1"
Pillow = "^9.4.0"
platformdirs = "^3.0.0"
pylint = "^2.16.3"
libsass = "^0.22.0"
jsmin = "^3.0.1"
cssmin = "^0.2.0"