Merge pull request #5 from Fluffy-Bean/testing

Format code with PyLint
This commit is contained in:
Michał 2023-03-04 21:51:24 +00:00 committed by GitHub
commit 7827c23402
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1399 additions and 1178 deletions

View file

@ -1,6 +1,8 @@
name: Pylint
on: [push]
on:
pull_request:
branches: [ main ]
jobs:
build:
@ -16,8 +18,8 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
python -m pip install --upgrade pip poetry
python -m poetry install
- name: Analysing the code with pylint
run: |
pylint $(git ls-files '*.py')
poetry run python3 -m pylint $(git ls-files '*.py')

View file

@ -16,6 +16,9 @@
<a href="https://github.com/Fluffy-Bean/onlylegs/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Fluffy-Bean/onlylegs?style=for-the-badge">
</a>
<a href="https://wakatime.com/badge/user/29bd1733-45f0-41c0-901e-d6daf49094d4/project/6aae41df-003f-4b17-ae8f-62cecfb3fc24">
<img src="https://wakatime.com/badge/user/29bd1733-45f0-41c0-901e-d6daf49094d4/project/6aae41df-003f-4b17-ae8f-62cecfb3fc24.svg?style=for-the-badge" alt="wakatime">
</a>
</div>
## Features

View file

@ -1,115 +1,131 @@
print("""
"""
___ _ _
/ _ \\ _ __ | |_ _| | ___ __ _ ___
| | | | '_ \\| | | | | | / _ \\/ _` / __|
| |_| | | | | | |_| | |__| __/ (_| \\__ \\
\\___/|_| |_|_|\\__, |_____\\___|\\__, |___/
/ _ \ _ __ | |_ _| | ___ __ _ ___
| | | | '_ \| | | | | | / _ \/ _` / __|
| |_| | | | | | |_| | |__| __/ (_| \__ \
\___/|_| |_|_|\__, |_____\___|\__, |___/
|___/ |___/
Created by Fluffy Bean - Version 23.03.03
""")
Created by Fluffy Bean - Version 23.03.04
"""
from flask import Flask, render_template
# Import system modules
import os
import sys
import logging
# Flask
from flask_compress import Compress
from flask import Flask, render_template
# Configuration
from dotenv import load_dotenv
import platformdirs
from gallery.logger import logger
logger.innit_logger()
import yaml
import os
from . import theme_manager
USER_DIR = platformdirs.user_config_dir('onlylegs')
INSTANCE_PATH = os.path.join(USER_DIR, 'instance')
# Check if any of the required files are missing
if not os.path.exists(platformdirs.user_config_dir('onlylegs')):
from .setup import setup
setup()
user_dir = platformdirs.user_config_dir('onlylegs')
instance_path = os.path.join(user_dir, 'instance')
from . import setup
setup.SetupApp()
# Get environment variables
if os.path.exists(os.path.join(user_dir, '.env')):
load_dotenv(os.path.join(user_dir, '.env'))
if os.path.exists(os.path.join(USER_DIR, '.env')):
load_dotenv(os.path.join(USER_DIR, '.env'))
print("Loaded environment variables")
else:
print("No environment variables found!")
exit(1)
sys.exit(1)
# Get config file
if os.path.exists(os.path.join(user_dir, 'conf.yml')):
with open(os.path.join(user_dir, 'conf.yml'), 'r') as f:
if os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as f:
conf = yaml.load(f, Loader=yaml.FullLoader)
print("Loaded gallery config")
else:
print("No config file found!")
exit(1)
sys.exit(1)
# Setup the logging config
LOGS_PATH = os.path.join(platformdirs.user_config_dir('onlylegs'), 'logs')
if not os.path.isdir(LOGS_PATH):
os.mkdir(LOGS_PATH)
logging.getLogger('werkzeug').disabled = True
logging.basicConfig(
filename=os.path.join(LOGS_PATH, 'only.log'),
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S',
format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',
encoding='utf-8')
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__,instance_path=instance_path)
"""
Create and configure the main app
"""
app = Flask(__name__,instance_path=INSTANCE_PATH)
compress = Compress()
# App configuration
app.config.from_mapping(
SECRET_KEY=os.environ.get('FLASK_SECRET'),
DATABASE=os.path.join(app.instance_path, 'gallery.sqlite'),
UPLOAD_FOLDER=os.path.join(user_dir, 'uploads'),
UPLOAD_FOLDER=os.path.join(USER_DIR, 'uploads'),
ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'],
MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'],
WEBSITE=conf['website'],
)
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('config.py', silent=True)
else:
# load the test config if passed in
app.config.from_mapping(test_config)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# Load theme
from . import sassy
sassy.compile('default', app.root_path)
theme_manager.CompileTheme('default', app.root_path)
@app.errorhandler(405)
def method_not_allowed(e):
def method_not_allowed(err):
error = '405'
msg = e.description
return render_template('error.html', error=error, msg=e), 404
msg = err.description
return render_template('error.html', error=error, msg=msg), 404
@app.errorhandler(404)
def page_not_found(e):
def page_not_found(err):
error = '404'
msg = e.description
msg = err.description
return render_template('error.html', error=error, msg=msg), 404
@app.errorhandler(403)
def forbidden(e):
def forbidden(err):
error = '403'
msg = e.description
msg = err.description
return render_template('error.html', error=error, msg=msg), 403
@app.errorhandler(410)
def gone(e):
def gone(err):
error = '410'
msg = e.description
msg = err.description
return render_template('error.html', error=error, msg=msg), 410
@app.errorhandler(500)
def internal_server_error(e):
def internal_server_error(err):
error = '500'
msg = e.description
msg = err.description
return render_template('error.html', error=error, msg=msg), 500
# Load login, registration and logout manager

View file

@ -1,38 +1,48 @@
from flask import Blueprint, current_app, send_from_directory, send_file, request, g, abort, flash, jsonify
"""
Onlylegs - API endpoints
Used intermally by the frontend and possibly by other applications
"""
from uuid import uuid4
import os
import io
import logging
from flask import (
Blueprint, current_app, send_from_directory, send_file, request, g, abort, flash, jsonify)
from werkzeug.utils import secure_filename
from PIL import Image, ImageOps # ImageFilter
from sqlalchemy.orm import sessionmaker
from gallery.auth import login_required
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 db # Import db to create a session
from . import metadata as mt
from .logger import logger
from uuid import uuid4
import io
import os
blueprint = Blueprint('api', __name__, url_prefix='/api')
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route('/uploads/<file>', methods=['GET'])
def uploads(file):
"""
Returns a file from the uploads folder
w and h are the width and height of the image for resizing
f is whether to apply filters to the image, such as blurring NSFW images
"""
# Get args
width = request.args.get('w', default=0, type=int) # Width of image
height = request.args.get('h', default=0, type=int) # Height of image
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters to image,
# such as blur for NSFW images
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters
# if no args are passed, return the raw file
if width == 0 and height == 0 and not filtered:
return send_from_directory(current_app.config['UPLOAD_FOLDER'],
secure_filename(file),
as_attachment=True)
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'],
secure_filename(file))):
abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file ,as_attachment=True)
# Of either width or height is 0, set it to the other value to keep aspect ratio
if width > 0 and height == 0:
@ -45,28 +55,27 @@ def uploads(file):
# Open image and set extension
try:
img = Image.open(
os.path.join(current_app.config['UPLOAD_FOLDER'],
secure_filename(file)))
except Exception as e:
logger.server(600, f"Error opening image: {e}")
img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'],file))
except FileNotFoundError:
logging.error('File not found: %s, possibly broken upload', file)
abort(404)
except Exception as err:
logging.error('Error opening image: %s', err)
abort(500)
img_ext = os.path.splitext(secure_filename(file))[-1].lower().replace(
'.', '')
img_ext = os.path.splitext(file)[-1].lower().replace('.', '')
img_ext = set_ext[img_ext]
img_icc = img.info.get(
"icc_profile") # Get ICC profile as it alters colours
# Get ICC profile as it alters colours when saving
img_icc = img.info.get("icc_profile")
# Resize image and orientate correctly
img.thumbnail((width, height), Image.LANCZOS)
img = ImageOps.exif_transpose(img)
# TODO: Add filters
# If has NSFW tag, blur image, etc.
if filtered:
#pass
img = img.filter(ImageFilter.GaussianBlur(20))
#img = img.filter(ImageFilter.GaussianBlur(20))
pass
try:
img.save(buff, img_ext, icc_profile=img_icc)
@ -75,8 +84,8 @@ def uploads(file):
# Convert to RGB and try again
img = img.convert('RGB')
img.save(buff, img_ext, icc_profile=img_icc)
except:
logger.server(600, f"Error resizing image: {file}")
except Exception as err:
logging.error('Could not resize image %s, error: %s', file, err)
abort(500)
img.close()
@ -89,47 +98,51 @@ def uploads(file):
@blueprint.route('/upload', methods=['POST'])
@login_required
def upload():
"""
Uploads an image to the server and saves it to the database
"""
form_file = request.files['file']
form = request.form
if not form_file:
return abort(404)
img_ext = os.path.splitext(secure_filename(
form_file.filename))[-1].replace('.', '').lower()
img_name = f"GWAGWA_{uuid4().__str__()}.{img_ext}"
img_ext = os.path.splitext(form_file.filename)[-1].replace('.', '').lower()
img_name = f"GWAGWA_{str(uuid4())}.{img_ext}"
if not img_ext in current_app.config['ALLOWED_EXTENSIONS'].keys():
logger.add(303, f"File extension not allowed: {img_ext}")
logging.info('File extension not allowed: %s', img_ext)
abort(403)
if os.path.isdir(current_app.config['UPLOAD_FOLDER']) == False:
if os.path.isdir(current_app.config['UPLOAD_FOLDER']) is False:
os.mkdir(current_app.config['UPLOAD_FOLDER'])
# Save to database
try:
tr = db.posts(img_name, form['description'], form['alt'], g.user.id)
db_session.add(tr)
db_session.add(db.posts(img_name, form['description'], form['alt'], g.user.id))
db_session.commit()
except Exception as e:
logger.server(600, f"Error saving to database: {e}")
except Exception as err:
logging.error('Could not save to database: %s', err)
abort(500)
# Save file
try:
form_file.save(
os.path.join(current_app.config['UPLOAD_FOLDER'], img_name))
except Exception as e:
logger.server(600, f"Error saving file: {e}")
except Exception as err:
logging.error('Could not save file: %s', err)
abort(500)
return 'Gwa Gwa'
@blueprint.route('/remove/<int:id>', methods=['POST'])
@blueprint.route('/remove/<int:img_id>', methods=['POST'])
@login_required
def remove(id):
img = db_session.query(db.posts).filter_by(id=id).first()
def remove(img_id):
"""
Deletes an image from the server and database
"""
img = db_session.query(db.posts).filter_by(id=img_id).first()
if img is None:
abort(404)
@ -137,34 +150,38 @@ def remove(id):
abort(403)
try:
os.remove(
os.path.join(current_app.config['UPLOAD_FOLDER'],
img.file_name))
except Exception as e:
logger.server(600, f"Error removing file: {e}")
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
except FileNotFoundError:
# File was already deleted or doesn't exist
logging.warning('File not found: %s, already deleted or never existed', img.file_name)
except Exception as err:
logging.error('Could not remove file: %s', err)
abort(500)
try:
db_session.query(db.posts).filter_by(id=id).delete()
db_session.query(db.posts).filter_by(id=img_id).delete()
db_session.commit()
except Exception as e:
logger.server(600, f"Error removing from database: {e}")
except Exception as err:
logging.error('Could not remove from database: %s', err)
abort(500)
logger.server(301, f"Removed image {id}")
logging.info('Removed image (%s) %s', img_id, img.file_name)
flash(['Image was all in Le Head!', 1])
return 'Gwa Gwa'
@blueprint.route('/metadata/<int:id>', methods=['GET'])
def metadata(id):
img = db_session.query(db.posts).filter_by(id=id).first()
@blueprint.route('/metadata/<int:img_id>', methods=['GET'])
def metadata(img_id):
"""
Yoinks metadata from an image
"""
img = db_session.query(db.posts).filter_by(id=img_id).first()
if img is None:
abort(404)
exif = mt.metadata.yoink(
os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name))
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
exif = mt.Metadata(img_path).yoink()
return jsonify(exif)
@ -172,12 +189,15 @@ def metadata(id):
@blueprint.route('/logfile')
@login_required
def logfile():
filename = logger.filename()
"""
Gets the log file and returns it as a JSON object
"""
filename = logging.getLoggerClass().root.handlers[0].baseFilename
log_dict = {}
i = 0
with open(filename) as f:
for line in f:
with open(filename, encoding='utf-8') as file:
for line in file:
line = line.split(' : ')
event = line[0].strip().split(' ')
@ -194,8 +214,11 @@ def logfile():
'code': int(message[1:4]),
'message': message[5:].strip()
}
except:
except ValueError:
message_data = {'code': 0, 'message': message}
except Exception as err:
logging.error('Could not parse log file: %s', err)
abort(500)
log_dict[i] = {'event': event_data, 'message': message_data}

View file

@ -1,35 +1,58 @@
"""
OnlyLegs - Authentification
User registration, login and logout and locking access to pages behind a login
"""
import re
import uuid
import logging
import functools
from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify, current_app
from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify
from werkzeug.security import check_password_hash, generate_password_hash
from gallery import db
from sqlalchemy.orm import sessionmaker
from sqlalchemy import exc
from gallery import db
blueprint = Blueprint('auth', __name__, url_prefix='/auth')
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
from .logger import logger
import re
import uuid
def login_required(view):
"""
Decorator to check if a user is logged in before accessing a page
"""
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None or session.get('uuid') is None:
logging.error('Authentification failed')
session.clear()
return redirect(url_for('gallery.index'))
blueprint = Blueprint('auth', __name__, url_prefix='/auth')
return view(**kwargs)
return wrapped_view
@blueprint.before_app_request
def load_logged_in_user():
"""
Runs before every request and checks if a user is logged in
"""
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 server logs with useless data
#add_log(103, 'Auth error before app request')
g.user = None
session.clear()
else:
is_alive = db_session.query(db.sessions).filter_by(session_uuid=user_uuid).first()
if is_alive is None:
logger.add(103, 'Session expired')
logging.info('Session expired')
flash(['Session expired!', '3'])
session.clear()
else:
@ -38,19 +61,24 @@ def load_logged_in_user():
@blueprint.route('/register', methods=['POST'])
def register():
"""
Register a new user
"""
username = request.form['username']
email = request.form['email']
password = request.form['password']
password_repeat = request.form['password-repeat']
error = []
if not username:
error.append('Username is empty!')
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
username_regex = re.compile(r'\b[A-Za-z0-9._%+-]+\b')
if not email:
error.append('Email is empty!')
elif not re.match(
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email):
if not username or not username_regex.match(username):
error.append('Username is invalid!')
if not email or not email_regex.match(email):
error.append('Email is invalid!')
if not password:
@ -59,73 +87,77 @@ def register():
error.append('Password is too short! Longer than 8 characters pls')
if not password_repeat:
error.append('Password repeat is empty!')
error.append('Enter password again!')
elif password_repeat != password:
error.append('Passwords do not match!')
if not error:
try:
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")
return 'gwa gwa'
if error:
return jsonify(error)
try:
db_session.add(db.users(username, email, generate_password_hash(password)))
db_session.commit()
except exc.IntegrityError:
return f'User {username} is already registered!'
except Exception as err:
logging.error('User %s could not be registered: %s', username, err)
return 'Something went wrong!'
logging.info('User %s registered', username)
return 'gwa gwa'
@blueprint.route('/login', methods=['POST'])
def login():
"""
Log in a registered user by adding the user id to the session
"""
username = request.form['username']
password = request.form['password']
error = None
user = db_session.query(db.users).filter_by(username=username).first()
error = []
if user is None:
logger.add(101, f"User {username} does not exist from {request.remote_addr}")
abort(403)
logging.error('User %s does not exist. Login attempt from %s',
username, request.remote_addr)
error.append('Username or Password is incorrect!')
elif not check_password_hash(user.password, password):
logger.add(102, f"User {username} password error from {request.remote_addr}")
logging.error('User %s entered wrong password. Login attempt from %s',
username, request.remote_addr)
error.append('Username or Password is incorrect!')
if error:
abort(403)
try:
session.clear()
session['user_id'] = user.id
session['uuid'] = str(uuid.uuid4())
tr = db.sessions(user.id, session.get('uuid'), request.remote_addr, request.user_agent.string, 1)
db_session.add(tr)
db_session.add(db.sessions(user.id,
session.get('uuid'),
request.remote_addr,
request.user_agent.string,
1))
db_session.commit()
except error as err:
logger.add(105, f"User {username} auth error: {err}")
except Exception as err:
logging.error('User %s could not be logged in: %s', username, err)
abort(500)
if error is None:
logger.add(100, f"User {username} logged in from {request.remote_addr}")
logging.info('User %s logged in from %s', username, request.remote_addr)
flash(['Logged in successfully!', '4'])
return 'gwa gwa'
abort(500)
@blueprint.route('/logout')
def logout():
logger.add(103, f"User {g.user.username} - id: {g.user.id} logged out")
"""
Clear the current session, including the stored user id
"""
logging.info('User (%s) %s logged out', session.get('user_id'), g.user.username)
session.clear()
return redirect(url_for('index'))
def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None or session.get('uuid') is None:
logger.add(103, "Auth error")
session.clear()
return redirect(url_for('gallery.index'))
return view(**kwargs)
return wrapped_view

View file

@ -1,6 +1,10 @@
"""
OnlyLegs - Database
Database models and functions for SQLAlchemy
"""
import os
import platformdirs
from datetime import datetime
import platformdirs
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import declarative_base, relationship
@ -11,7 +15,11 @@ engine = create_engine(f'sqlite:///{path_to_db}', echo=False)
base = declarative_base()
class users (base):
class users (base): # pylint: disable=too-few-public-methods, C0103
"""
User table
Joins with post, groups, session and log
"""
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
@ -31,7 +39,12 @@ class users (base):
self.password = password
self.created_at = datetime.now()
class posts (base):
class posts (base): # pylint: disable=too-few-public-methods, C0103
"""
Post table
Joins with group_junction
"""
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
@ -50,7 +63,12 @@ class posts (base):
self.author_id = author_id
self.created_at = datetime.now()
class groups (base):
class groups (base): # pylint: disable=too-few-public-methods, C0103
"""
Group table
Joins with group_junction
"""
__tablename__ = 'groups'
id = Column(Integer, primary_key=True)
@ -67,7 +85,12 @@ class groups (base):
self.author_id = author_id
self.created_at = datetime.now()
class group_junction (base):
class group_junction (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)
@ -78,53 +101,67 @@ class group_junction (base):
self.group_id = group_id
self.post_id = post_id
class sessions (base):
class sessions (base): # pylint: disable=too-few-public-methods, C0103
"""
Session table
Joins with user
"""
__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)
ip_address = 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):
def __init__(self, user_id, session_uuid, ip_address, user_agent, active): # pylint: disable=too-many-arguments, C0103
self.user_id = user_id
self.session_uuid = session_uuid
self.ip = ip
self.ip_address = ip_address
self.user_agent = user_agent
self.active = active
self.created_at = datetime.now()
class logs (base):
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 = Column(String, nullable=False)
ip_address = 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):
def __init__(self, user_id, ip_address, code, msg):
self.user_id = user_id
self.ip = ip
self.ip_address = ip_address
self.code = code
self.msg = msg
self.created_at = datetime.now()
class bans (base):
class bans (base): # pylint: disable=too-few-public-methods, C0103
"""
Bans table
"""
__tablename__ = 'bans'
id = Column(Integer, primary_key=True)
ip = Column(String, nullable=False)
ip_address = 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
def __init__(self, ip_address, code, msg):
self.ip_address = ip_address
self.code = code
self.msg = msg
self.created_at = datetime.now()

View file

@ -1,112 +0,0 @@
import logging
import os
from datetime import datetime
import platformdirs
# Prevent werkzeug from logging
logging.getLogger('werkzeug').disabled = True
class logger:
def innit_logger():
filepath = os.path.join(platformdirs.user_config_dir('onlylegs'), 'logs')
#filename = f'onlylogs_{datetime.now().strftime("%Y%m%d")}.log'
filename = 'only.log'
if not os.path.isdir(filepath):
os.mkdir(filepath)
logging.basicConfig(
filename=os.path.join(filepath, filename),
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S',
format=
'%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',
encoding='utf-8')
"""
Login and Auth error codes
--------------------------
100: Login
101: Login attempt
102: Login attempt (password error)
103: Logout
104: Registration
105: Auth error
Account error codes - User actions
----------------------------------
200: Account password reset
201: Account email change
202: Account delete
203: Account error
Image error codes
-----------------
300: Image upload
301: Image delete
302: Image edit
303: Image error
Group error codes
-----------------
400: Group create
401: Group delete
402: Group edit
403: Group error
User error codes - Admin actions
--------------------------------
500: User delete
501: User edit
502: User ban
503: User unban
504: User permission change
505: User error
Server and Website errors - Internal
------------------------------------
600: Server error
601: Server crash
602: Website error
603: Website crash
604: Maintenance
605: Startup
606: Other
621: :3
"""
def add(error, message):
# Allowed error codes, as listed above
log_levels = [
100, 101, 102, 103, 104, 105, 200, 201, 202, 203, 300, 301, 302,
303, 400, 401, 402, 403, 500, 501, 502, 503, 504, 505
]
if error in log_levels:
logging.log(logging.INFO, f'[{error}] {message}')
else:
logging.log(logging.WARN, f'[606] Improper use of error code {error}')
def server(error, message):
log_levels = {
600: logging.ERROR,
601: logging.CRITICAL,
602: logging.ERROR,
603: logging.CRITICAL,
604: logging.DEBUG,
605: logging.DEBUG,
606: logging.INFO,
621: logging.INFO,
}
if error in log_levels:
logging.log(log_levels[error], f'[{error}] {message}')
else:
logging.log(logging.WARN, f'[606] Invalid error code {error}')
def filename():
handler = logging.getLogger().handlers[0]
filename = handler.baseFilename
return filename

View file

@ -1,705 +0,0 @@
import PIL
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
from datetime import datetime
import os
class metadata:
def yoink(filename):
exif = metadata.getFile(filename)
file_size = os.path.getsize(filename)
file_name = os.path.basename(filename)
file_resolution = Image.open(filename).size
if exif:
unformatted_exif = metadata.format(exif, file_size, file_name,
file_resolution)
else:
# No EXIF data, get some basic informaton from the file
unformatted_exif = {
'File': {
'Name': {
'type': 'text',
'raw': file_name
},
'Size': {
'type': 'number',
'raw': file_size,
'formatted': metadata.human_size(file_size)
},
'Format': {
'type': 'text',
'raw': file_name.split('.')[-1]
},
'Width': {
'type': 'number',
'raw': file_resolution[0]
},
'Height': {
'type': 'number',
'raw': file_resolution[1]
},
}
}
formatted_exif = {}
for section in unformatted_exif:
tmp = {}
for value in unformatted_exif[section]:
if unformatted_exif[section][value]['raw'] != None:
raw_type = unformatted_exif[section][value]['raw']
if isinstance(raw_type, PIL.TiffImagePlugin.IFDRational):
raw_type = raw_type.__float__()
elif isinstance(raw_type, bytes):
raw_type = raw_type.decode('utf-8')
tmp[value] = unformatted_exif[section][value]
if len(tmp) > 0:
formatted_exif[section] = tmp
return formatted_exif
def getFile(filename):
try:
file = Image.open(filename)
raw = file._getexif()
exif = {}
for tag, value in TAGS.items():
if tag in raw:
data = raw[tag]
else:
data = None
exif[value] = {"tag": tag, "raw": data}
file.close()
return exif
except Exception as e:
return False
def format(raw, file_size, file_name, file_resolution):
exif = {}
exif['Photographer'] = {
'Artist': {
'type': 'text',
'raw': raw["Artist"]["raw"]
},
'Comment': {
'type': 'text',
'raw': raw["UserComment"]["raw"]
},
'Description': {
'type': 'text',
'raw': raw["ImageDescription"]["raw"]
},
'Copyright': {
'type': 'text',
'raw': raw["Copyright"]["raw"]
},
}
exif['Camera'] = {
'Model': {
'type': 'text',
'raw': raw['Model']['raw']
},
'Make': {
'type': 'text',
'raw': raw['Make']['raw']
},
'Camera Type': {
'type': 'text',
'raw': raw['BodySerialNumber']['raw']
},
'Lens Make': {
'type': 'text',
'raw': raw['LensMake']['raw'],
},
'Lense Model': {
'type': 'text',
'raw': raw['LensModel']['raw'],
},
'Lense Spec': {
'type':
'text',
'raw':
raw['LensSpecification']['raw'],
'formatted':
metadata.lensSpecification(raw['LensSpecification']['raw'])
},
'Component Config': {
'type':
'text',
'raw':
raw['ComponentsConfiguration']['raw'],
'formatted':
metadata.componentsConfiguration(
raw['ComponentsConfiguration']['raw'])
},
'Date Processed': {
'type': 'date',
'raw': raw['DateTime']['raw'],
'formatted': metadata.date(raw['DateTime']['raw'])
},
'Date Digitized': {
'type': 'date',
'raw': raw["DateTimeDigitized"]["raw"],
'formatted': metadata.date(raw["DateTimeDigitized"]["raw"])
},
'Time Offset': {
'type': 'text',
'raw': raw["OffsetTime"]["raw"]
},
'Time Offset - Original': {
'type': 'text',
'raw': raw["OffsetTimeOriginal"]["raw"]
},
'Time Offset - Digitized': {
'type': 'text',
'raw': raw["OffsetTimeDigitized"]["raw"]
},
'Date Original': {
'type': 'date',
'raw': raw["DateTimeOriginal"]["raw"],
'formatted': metadata.date(raw["DateTimeOriginal"]["raw"])
},
'FNumber': {
'type': 'fnumber',
'raw': raw["FNumber"]["raw"],
'formatted': metadata.fnumber(raw["FNumber"]["raw"])
},
'Focal Length': {
'type': 'focal',
'raw': raw["FocalLength"]["raw"],
'formatted': metadata.focal(raw["FocalLength"]["raw"])
},
'Focal Length (35mm format)': {
'type': 'focal',
'raw': raw["FocalLengthIn35mmFilm"]["raw"],
'formatted':
metadata.focal(raw["FocalLengthIn35mmFilm"]["raw"])
},
'Max Aperture': {
'type': 'fnumber',
'raw': raw["MaxApertureValue"]["raw"],
'formatted': metadata.fnumber(raw["MaxApertureValue"]["raw"])
},
'Aperture': {
'type': 'fnumber',
'raw': raw["ApertureValue"]["raw"],
'formatted': metadata.fnumber(raw["ApertureValue"]["raw"])
},
'Shutter Speed': {
'type': 'shutter',
'raw': raw["ShutterSpeedValue"]["raw"],
'formatted': metadata.shutter(raw["ShutterSpeedValue"]["raw"])
},
'ISO Speed Ratings': {
'type': 'number',
'raw': raw["ISOSpeedRatings"]["raw"],
'formatted': metadata.iso(raw["ISOSpeedRatings"]["raw"])
},
'ISO Speed': {
'type': 'iso',
'raw': raw["ISOSpeed"]["raw"],
'formatted': metadata.iso(raw["ISOSpeed"]["raw"])
},
'Sensitivity Type': {
'type':
'number',
'raw':
raw["SensitivityType"]["raw"],
'formatted':
metadata.sensitivityType(raw["SensitivityType"]["raw"])
},
'Exposure Bias': {
'type': 'ev',
'raw': raw["ExposureBiasValue"]["raw"],
'formatted': metadata.ev(raw["ExposureBiasValue"]["raw"])
},
'Exposure Time': {
'type': 'shutter',
'raw': raw["ExposureTime"]["raw"],
'formatted': metadata.shutter(raw["ExposureTime"]["raw"])
},
'Exposure Mode': {
'type': 'number',
'raw': raw["ExposureMode"]["raw"],
'formatted': metadata.exposureMode(raw["ExposureMode"]["raw"])
},
'Exposure Program': {
'type':
'number',
'raw':
raw["ExposureProgram"]["raw"],
'formatted':
metadata.exposureProgram(raw["ExposureProgram"]["raw"])
},
'White Balance': {
'type': 'number',
'raw': raw["WhiteBalance"]["raw"],
'formatted': metadata.whiteBalance(raw["WhiteBalance"]["raw"])
},
'Flash': {
'type': 'number',
'raw': raw["Flash"]["raw"],
'formatted': metadata.flash(raw["Flash"]["raw"])
},
'Metering Mode': {
'type': 'number',
'raw': raw["MeteringMode"]["raw"],
'formatted': metadata.meteringMode(raw["MeteringMode"]["raw"])
},
'Light Source': {
'type': 'number',
'raw': raw["LightSource"]["raw"],
'formatted': metadata.lightSource(raw["LightSource"]["raw"])
},
'Scene Capture Type': {
'type':
'number',
'raw':
raw["SceneCaptureType"]["raw"],
'formatted':
metadata.sceneCaptureType(raw["SceneCaptureType"]["raw"])
},
'Scene Type': {
'type': 'number',
'raw': raw["SceneType"]["raw"],
'formatted': metadata.sceneType(raw["SceneType"]["raw"])
},
'Rating': {
'type': 'number',
'raw': raw["Rating"]["raw"],
'formatted': metadata.rating(raw["Rating"]["raw"])
},
'Rating Percent': {
'type': 'number',
'raw': raw["RatingPercent"]["raw"],
'formatted':
metadata.ratingPercent(raw["RatingPercent"]["raw"])
},
}
exif['Software'] = {
'Software': {
'type': 'text',
'raw': raw['Software']['raw']
},
'Colour Space': {
'type': 'number',
'raw': raw['ColorSpace']['raw'],
'formatted': metadata.colorSpace(raw['ColorSpace']['raw'])
},
'Compression': {
'type': 'number',
'raw': raw['Compression']['raw'],
'formatted': metadata.compression(raw['Compression']['raw'])
},
}
exif['File'] = {
'Name': {
'type': 'text',
'raw': file_name
},
'Size': {
'type': 'number',
'raw': file_size,
'formatted': metadata.human_size(file_size)
},
'Format': {
'type': 'text',
'raw': file_name.split('.')[-1]
},
'Width': {
'type': 'number',
'raw': file_resolution[0]
},
'Height': {
'type': 'number',
'raw': file_resolution[1]
},
'Orientation': {
'type': 'number',
'raw': raw["Orientation"]["raw"],
'formatted': metadata.orientation(raw["Orientation"]["raw"])
},
'Xresolution': {
'type': 'number',
'raw': raw["XResolution"]["raw"]
},
'Yresolution': {
'type': 'number',
'raw': raw["YResolution"]["raw"]
},
'Resolution Units': {
'type': 'number',
'raw': raw["ResolutionUnit"]["raw"],
'formatted':
metadata.resolutionUnit(raw["ResolutionUnit"]["raw"])
},
}
#exif['Raw'] = {}
#for key in raw:
# try:
# exif['Raw'][key] = {
# 'type': 'text',
# 'raw': raw[key]['raw'].decode('utf-8')
# }
# except:
# exif['Raw'][key] = {
# 'type': 'text',
# 'raw': str(raw[key]['raw'])
# }
return exif
def human_size(num, suffix="B"):
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
return f"{num:3.1f}{unit}{suffix}"
num /= 1024.0
return f"{num:.1f}Yi{suffix}"
def date(date):
date_format = '%Y:%m:%d %H:%M:%S'
if date:
return str(datetime.strptime(date, date_format))
else:
return None
def fnumber(value):
if value != None:
return 'f/' + str(value)
else:
return None
def iso(value):
if value != None:
return 'ISO ' + str(value)
else:
return None
def shutter(value):
if value != None:
return str(value) + 's'
else:
return None
def focal(value):
if value != None:
try:
return str(value[0] / value[1]) + 'mm'
except:
return str(value) + 'mm'
else:
return None
def ev(value):
if value != None:
return str(value) + 'EV'
else:
return None
def colorSpace(value):
types = {1: 'sRGB', 65535: 'Uncalibrated', 0: 'Reserved'}
try:
return types[int(value)]
except:
return None
def flash(value):
types = {
0:
'Flash did not fire',
1:
'Flash fired',
5:
'Strobe return light not detected',
7:
'Strobe return light detected',
9:
'Flash fired, compulsory flash mode',
13:
'Flash fired, compulsory flash mode, return light not detected',
15:
'Flash fired, compulsory flash mode, return light detected',
16:
'Flash did not fire, compulsory flash mode',
24:
'Flash did not fire, auto mode',
25:
'Flash fired, auto mode',
29:
'Flash fired, auto mode, return light not detected',
31:
'Flash fired, auto mode, return light detected',
32:
'No flash function',
65:
'Flash fired, red-eye reduction mode',
69:
'Flash fired, red-eye reduction mode, return light not detected',
71:
'Flash fired, red-eye reduction mode, return light detected',
73:
'Flash fired, compulsory flash mode, red-eye reduction mode',
77:
'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
79:
'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
89:
'Flash fired, auto mode, red-eye reduction mode',
93:
'Flash fired, auto mode, return light not detected, red-eye reduction mode',
95:
'Flash fired, auto mode, return light detected, red-eye reduction mode'
}
try:
return types[int(value)]
except:
return None
def exposureProgram(value):
types = {
0: 'Not defined',
1: 'Manual',
2: 'Normal program',
3: 'Aperture priority',
4: 'Shutter priority',
5: 'Creative program',
6: 'Action program',
7: 'Portrait mode',
8: 'Landscape mode'
}
try:
return types[int(value)]
except:
return None
def meteringMode(value):
types = {
0: 'Unknown',
1: 'Average',
2: 'Center-Weighted Average',
3: 'Spot',
4: 'Multi-Spot',
5: 'Pattern',
6: 'Partial',
255: 'Other'
}
try:
return types[int(value)]
except:
return None
def resolutionUnit(value):
types = {
1: 'No absolute unit of measurement',
2: 'Inch',
3: 'Centimeter'
}
try:
return types[int(value)]
except:
return None
def lightSource(value):
types = {
0: 'Unknown',
1: 'Daylight',
2: 'Fluorescent',
3: 'Tungsten (incandescent light)',
4: 'Flash',
9: 'Fine weather',
10: 'Cloudy weather',
11: 'Shade',
12: 'Daylight fluorescent (D 5700 - 7100K)',
13: 'Day white fluorescent (N 4600 - 5400K)',
14: 'Cool white fluorescent (W 3900 - 4500K)',
15: 'White fluorescent (WW 3200 - 3700K)',
17: 'Standard light A',
18: 'Standard light B',
19: 'Standard light C',
20: 'D55',
21: 'D65',
22: 'D75',
23: 'D50',
24: 'ISO studio tungsten',
255: 'Other light source',
}
try:
return types[int(value)]
except:
return None
def sceneCaptureType(value):
types = {
0: 'Standard',
1: 'Landscape',
2: 'Portrait',
3: 'Night scene',
}
try:
return types[int(value)]
except:
return None
def sceneType(value):
if value:
return 'Directly photographed image'
else:
return None
def whiteBalance(value):
types = {
0: 'Auto white balance',
1: 'Manual white balance',
}
try:
return types[int(value)]
except:
return None
def exposureMode(value):
types = {
0: 'Auto exposure',
1: 'Manual exposure',
2: 'Auto bracket',
}
try:
return types[int(value)]
except:
return None
def sensitivityType(value):
types = {
0:
'Unknown',
1:
'Standard Output Sensitivity',
2:
'Recommended Exposure Index',
3:
'ISO Speed',
4:
'Standard Output Sensitivity and Recommended Exposure Index',
5:
'Standard Output Sensitivity and ISO Speed',
6:
'Recommended Exposure Index and ISO Speed',
7:
'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed',
}
try:
return types[int(value)]
except:
return None
def lensSpecification(value):
if value:
return str(value[0] / value[1]) + 'mm - ' + str(
value[2] / value[3]) + 'mm'
else:
return None
def compression(value):
types = {
1: 'Uncompressed',
2: 'CCITT 1D',
3: 'T4/Group 3 Fax',
4: 'T6/Group 4 Fax',
5: 'LZW',
6: 'JPEG (old-style)',
7: 'JPEG',
8: 'Adobe Deflate',
9: 'JBIG B&W',
10: 'JBIG Color',
99: 'JPEG',
262: 'Kodak 262',
32766: 'Next',
32767: 'Sony ARW Compressed',
32769: 'Packed RAW',
32770: 'Samsung SRW Compressed',
32771: 'CCIRLEW',
32772: 'Samsung SRW Compressed 2',
32773: 'PackBits',
32809: 'Thunderscan',
32867: 'Kodak KDC Compressed',
32895: 'IT8CTPAD',
32896: 'IT8LW',
32897: 'IT8MP',
32898: 'IT8BL',
32908: 'PixarFilm',
32909: 'PixarLog',
32946: 'Deflate',
32947: 'DCS',
33003: 'Aperio JPEG 2000 YCbCr',
33005: 'Aperio JPEG 2000 RGB',
34661: 'JBIG',
34676: 'SGILog',
34677: 'SGILog24',
34712: 'JPEG 2000',
34713: 'Nikon NEF Compressed',
34715: 'JBIG2 TIFF FX',
34718: '(MDI) Binary Level Codec',
34719: '(MDI) Progressive Transform Codec',
34720: '(MDI) Vector',
34887: 'ESRI Lerc',
34892: 'Lossy JPEG',
34925: 'LZMA2',
34926: 'Zstd',
34927: 'WebP',
34933: 'PNG',
34934: 'JPEG XR',
65000: 'Kodak DCR Compressed',
65535: 'Pentax PEF Compressed',
}
try:
return types[int(value)]
except:
return None
def orientation(value):
types = {
1: 'Horizontal (normal)',
2: 'Mirror horizontal',
3: 'Rotate 180',
4: 'Mirror vertical',
5: 'Mirror horizontal and rotate 270 CW',
6: 'Rotate 90 CW',
7: 'Mirror horizontal and rotate 90 CW',
8: 'Rotate 270 CW',
}
try:
return types[int(value)]
except:
return None
def componentsConfiguration(value):
types = {
0: '',
1: 'Y',
2: 'Cb',
3: 'Cr',
4: 'R',
5: 'G',
6: 'B',
}
try:
return ''.join([types[int(x)] for x in value])
except:
return None
def rating(value):
return str(value) + ' stars'
def ratingPercent(value):
return str(value) + '%'

View file

@ -0,0 +1,117 @@
"""
OnlyLegs - Metatada Parser
Parse metadata from images if available
otherwise get some basic information from the file
"""
import os
import logging
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
from .helpers import *
from .mapping import *
class Metadata:
"""
Metadata parser
"""
def __init__(self, file_path):
"""
Initialize the metadata parser
"""
self.file_path = file_path
img_exif = {}
try:
file = Image.open(file_path)
tags = file._getexif()
img_exif = {}
for tag, value in TAGS.items():
if tag in tags:
img_exif[value] = tags[tag]
img_exif['FileName'] = os.path.basename(file_path)
img_exif['FileSize'] = os.path.getsize(file_path)
img_exif['FileFormat'] = img_exif['FileName'].split('.')[-1]
img_exif['FileWidth'], img_exif['FileHeight'] = file.size
file.close()
except TypeError:
img_exif['FileName'] = os.path.basename(file_path)
img_exif['FileSize'] = os.path.getsize(file_path)
img_exif['FileFormat'] = img_exif['FileName'].split('.')[-1]
img_exif['FileWidth'], img_exif['FileHeight'] = file.size
self.encoded = img_exif
def yoink(self):
"""
Yoinks the metadata from the image
"""
if not os.path.isfile(self.file_path):
return None
return self.format_data(self.encoded)
def format_data(self, encoded_exif): # pylint: disable=R0912 # For now, this is fine
"""
Formats the data into a dictionary
"""
exif = {
'Photographer': {},
'Camera': {},
'Software': {},
'File': {},
}
for data in encoded_exif:
if data in PHOTOGRAHER_MAPPING:
exif['Photographer'][PHOTOGRAHER_MAPPING[data][0]] = {
'raw': encoded_exif[data],
}
elif data in CAMERA_MAPPING:
if len(CAMERA_MAPPING[data]) == 2:
exif['Camera'][CAMERA_MAPPING[data][0]] = {
'raw': encoded_exif[data],
'formatted':
getattr(helpers, CAMERA_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
}
else:
exif['Camera'][CAMERA_MAPPING[data][0]] = {
'raw': encoded_exif[data],
}
elif data in SOFTWARE_MAPPING:
if len(SOFTWARE_MAPPING[data]) == 2:
exif['Software'][SOFTWARE_MAPPING[data][0]] = {
'raw': encoded_exif[data],
'formatted':
getattr(helpers, SOFTWARE_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
}
else:
exif['Software'][SOFTWARE_MAPPING[data][0]] = {
'raw': encoded_exif[data],
}
elif data in FILE_MAPPING:
if len(FILE_MAPPING[data]) == 2:
exif['File'][FILE_MAPPING[data][0]] = {
'raw': encoded_exif[data],
'formatted':
getattr(helpers, FILE_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
}
else:
exif['File'][FILE_MAPPING[data][0]] = {
'raw': encoded_exif[data]
}
# Remove empty keys
if len(exif['Photographer']) == 0:
del exif['Photographer']
if len(exif['Camera']) == 0:
del exif['Camera']
if len(exif['Software']) == 0:
del exif['Software']
if len(exif['File']) == 0:
del exif['File']
return exif

407
gallery/metadata/helpers.py Normal file
View file

@ -0,0 +1,407 @@
"""
OnlyLegs - Metadata Parser
Metadata formatting helpers
"""
from datetime import datetime
def human_size(value):
"""
Formats the size of a file in a human readable format
"""
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(value) < 1024.0:
return f"{value:3.1f}{unit}B"
value /= 1024.0
return f"{value:.1f}YiB"
def date_format(value):
"""
Formats the date into a standard format
"""
return str(datetime.strptime(value, '%Y:%m:%d %H:%M:%S'))
def fnumber(value):
"""
Formats the f-number into a standard format
"""
return 'f/' + str(value)
def iso(value):
"""
Formats the ISO into a standard format
"""
return 'ISO ' + str(value)
def shutter(value):
"""
Formats the shutter speed into a standard format
"""
return str(value) + 's'
def focal_length(value):
"""
Formats the focal length into a standard format
"""
try:
return str(value[0] / value[1]) + 'mm'
except TypeError:
return str(value) + 'mm'
def exposure(value):
"""
Formats the exposure value into a standard format
"""
return str(value) + 'EV'
def color_space(value):
"""
Maps the value of the color space to a human readable format
"""
value_map = {
0: 'Reserved',
1: 'sRGB',
65535: 'Uncalibrated'
}
try:
return value_map[int(value)]
except KeyError:
return None
def flash(value):
"""
Maps the value of the flash to a human readable format
"""
value_map = {
0: 'Flash did not fire',
1: 'Flash fired',
5: 'Strobe return light not detected',
7: 'Strobe return light detected',
9: 'Flash fired, compulsory flash mode',
13: 'Flash fired, compulsory flash mode, return light not detected',
15: 'Flash fired, compulsory flash mode, return light detected',
16: 'Flash did not fire, compulsory flash mode',
24: 'Flash did not fire, auto mode',
25: 'Flash fired, auto mode',
29: 'Flash fired, auto mode, return light not detected',
31: 'Flash fired, auto mode, return light detected',
32: 'No flash function',
65: 'Flash fired, red-eye reduction mode',
69: 'Flash fired, red-eye reduction mode, return light not detected',
71: 'Flash fired, red-eye reduction mode, return light detected',
73: 'Flash fired, compulsory flash mode, red-eye reduction mode',
77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
89: 'Flash fired, auto mode, red-eye reduction mode',
93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'
}
try:
return value_map[int(value)]
except KeyError:
return None
def exposure_program(value):
"""
Maps the value of the exposure program to a human readable format
"""
value_map = {
0: 'Not defined',
1: 'Manual',
2: 'Normal program',
3: 'Aperture priority',
4: 'Shutter priority',
5: 'Creative program',
6: 'Action program',
7: 'Portrait mode',
8: 'Landscape mode'
}
try:
return value_map[int(value)]
except KeyError:
return None
def metering_mode(value):
"""
Maps the value of the metering mode to a human readable format
"""
value_map = {
0: 'Unknown',
1: 'Average',
2: 'Center-Weighted Average',
3: 'Spot',
4: 'Multi-Spot',
5: 'Pattern',
6: 'Partial',
255: 'Other'
}
try:
return value_map[int(value)]
except KeyError:
return None
def resolution_unit(value):
"""
Maps the value of the resolution unit to a human readable format
"""
value_map = {
1: 'No absolute unit of measurement',
2: 'Inch',
3: 'Centimeter'
}
try:
return value_map[int(value)]
except KeyError:
return None
def light_source(value):
"""
Maps the value of the light source to a human readable format
"""
value_map = {
0: 'Unknown',
1: 'Daylight',
2: 'Fluorescent',
3: 'Tungsten (incandescent light)',
4: 'Flash',
9: 'Fine weather',
10: 'Cloudy weather',
11: 'Shade',
12: 'Daylight fluorescent (D 5700 - 7100K)',
13: 'Day white fluorescent (N 4600 - 5400K)',
14: 'Cool white fluorescent (W 3900 - 4500K)',
15: 'White fluorescent (WW 3200 - 3700K)',
17: 'Standard light A',
18: 'Standard light B',
19: 'Standard light C',
20: 'D55',
21: 'D65',
22: 'D75',
23: 'D50',
24: 'ISO studio tungsten',
255: 'Other light source',
}
try:
return value_map[int(value)]
except KeyError:
return None
def scene_capture_type(value):
"""
Maps the value of the scene capture type to a human readable format
"""
value_map = {
0: 'Standard',
1: 'Landscape',
2: 'Portrait',
3: 'Night scene',
}
try:
return value_map[int(value)]
except KeyError:
return None
def scene_type(value): # pylint: disable=W0613 # Itss fiiiineeee
"""
Maps the value of the scene type to a human readable format
"""
return 'Directly photographed image'
def white_balance(value):
"""
Maps the value of the white balance to a human readable format
"""
value_map = {
0: 'Auto white balance',
1: 'Manual white balance',
}
try:
return value_map[int(value)]
except KeyError:
return None
def exposure_mode(value):
"""
Maps the value of the exposure mode to a human readable format
"""
value_map = {
0: 'Auto exposure',
1: 'Manual exposure',
2: 'Auto bracket',
}
try:
return value_map[int(value)]
except KeyError:
return None
def sensitivity_type(value):
"""
Maps the value of the sensitivity type to a human readable format
"""
value_map = {
0:
'Unknown',
1:
'Standard Output Sensitivity',
2:
'Recommended Exposure Index',
3:
'ISO Speed',
4:
'Standard Output Sensitivity and Recommended Exposure Index',
5:
'Standard Output Sensitivity and ISO Speed',
6:
'Recommended Exposure Index and ISO Speed',
7:
'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed',
}
try:
return value_map[int(value)]
except KeyError:
return None
def lens_specification(value):
"""
Maps the value of the lens specification to a human readable format
"""
return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm'
def compression_type(value):
"""
Maps the value of the compression type to a human readable format
"""
value_map = {
1: 'Uncompressed',
2: 'CCITT 1D',
3: 'T4/Group 3 Fax',
4: 'T6/Group 4 Fax',
5: 'LZW',
6: 'JPEG (old-style)',
7: 'JPEG',
8: 'Adobe Deflate',
9: 'JBIG B&W',
10: 'JBIG Color',
99: 'JPEG',
262: 'Kodak 262',
32766: 'Next',
32767: 'Sony ARW Compressed',
32769: 'Packed RAW',
32770: 'Samsung SRW Compressed',
32771: 'CCIRLEW',
32772: 'Samsung SRW Compressed 2',
32773: 'PackBits',
32809: 'Thunderscan',
32867: 'Kodak KDC Compressed',
32895: 'IT8CTPAD',
32896: 'IT8LW',
32897: 'IT8MP',
32898: 'IT8BL',
32908: 'PixarFilm',
32909: 'PixarLog',
32946: 'Deflate',
32947: 'DCS',
33003: 'Aperio JPEG 2000 YCbCr',
33005: 'Aperio JPEG 2000 RGB',
34661: 'JBIG',
34676: 'SGILog',
34677: 'SGILog24',
34712: 'JPEG 2000',
34713: 'Nikon NEF Compressed',
34715: 'JBIG2 TIFF FX',
34718: '(MDI) Binary Level Codec',
34719: '(MDI) Progressive Transform Codec',
34720: '(MDI) Vector',
34887: 'ESRI Lerc',
34892: 'Lossy JPEG',
34925: 'LZMA2',
34926: 'Zstd',
34927: 'WebP',
34933: 'PNG',
34934: 'JPEG XR',
65000: 'Kodak DCR Compressed',
65535: 'Pentax PEF Compressed',
}
try:
return value_map[int(value)]
except KeyError:
return None
def orientation(value):
"""
Maps the value of the orientation to a human readable format
"""
value_map = {
1: 'Horizontal (normal)',
2: 'Mirror horizontal',
3: 'Rotate 180',
4: 'Mirror vertical',
5: 'Mirror horizontal and rotate 270 CW',
6: 'Rotate 90 CW',
7: 'Mirror horizontal and rotate 90 CW',
8: 'Rotate 270 CW',
}
try:
return value_map[int(value)]
except KeyError:
return None
def components_configuration(value):
"""
Maps the value of the components configuration to a human readable format
"""
value_map = {
0: '',
1: 'Y',
2: 'Cb',
3: 'Cr',
4: 'R',
5: 'G',
6: 'B',
}
try:
return ''.join([value_map[int(x)] for x in value])
except KeyError:
return None
def rating(value):
"""
Maps the value of the rating to a human readable format
"""
return str(value) + ' stars'
def rating_percent(value):
"""
Maps the value of the rating to a human readable format
"""
return str(value) + '%'
def pixel_dimension(value):
"""
Maps the value of the pixel dimension to a human readable format
"""
return str(value) + 'px'

View file

@ -0,0 +1,62 @@
"""
OnlyLegs - Metatada Parser
Mapping for metadata
"""
PHOTOGRAHER_MAPPING = {
'Artist': ['Artist'],
'UserComment': ['Comment'],
'ImageDescription': ['Description'],
'Copyright': ['Copyright'],
}
CAMERA_MAPPING = {
'Model': ['Model'],
'Make': ['Make'],
'BodySerialNumber': ['Camera Type'],
'LensMake': ['Lens Make'],
'LenseModel': ['Lens Model'],
'LensSpecification': ['Lens Specification', 'lens_specification'],
'ComponentsConfiguration': ['Components Configuration', 'components_configuration'],
'DateTime': ['Date Processed', 'date_format'],
'DateTimeDigitized': ['Time Digitized', 'date_format'],
'OffsetTime': ['Time Offset'],
'OffsetTimeOriginal': ['Time Offset - Original'],
'OffsetTimeDigitized': ['Time Offset - Digitized'],
'DateTimeOriginal': ['Date Original', 'date_format'],
'FNumber': ['F-Stop', 'fnumber'],
'FocalLength': ['Focal Length', 'focal_length'],
'FocalLengthIn35mmFilm': ['Focal Length (35mm format)', 'focal_length'],
'MaxApertureValue': ['Max Aperture', 'fnumber'],
'ApertureValue': ['Aperture', 'fnumber'],
'ShutterSpeedValue': ['Shutter Speed', 'shutter'],
'ISOSpeedRatings': ['ISO Speed Ratings', 'iso'],
'ISOSpeed': ['ISO Speed', 'iso'],
'SensitivityType': ['Sensitivity Type', 'sensitivity_type'],
'ExposureBiasValue': ['Exposure Bias', 'ev'],
'ExposureTime': ['Exposure Time', 'shutter'],
'ExposureMode': ['Exposure Mode', 'exposure_mode'],
'ExposureProgram': ['Exposure Program', 'exposure_program'],
'WhiteBalance': ['White Balance', 'white_balance'],
'Flash': ['Flash', 'flash'],
'MeteringMode': ['Metering Mode', 'metering_mode'],
'LightSource': ['Light Source', 'light_source'],
'SceneCaptureType': ['Scene Capture Type', 'scene_capture_type'],
'SceneType': ['Scene Type', 'scene_type'],
'Rating': ['Rating', 'rating'],
'RatingPercent': ['Rating Percent', 'rating_percent'],
}
SOFTWARE_MAPPING = {
'Software': ['Software'],
'ColorSpace': ['Colour Space', 'color_space'],
'Compression': ['Compression', 'compression_type'],
}
FILE_MAPPING = {
'FileName': ['Name'],
'FileSize': ['Size', 'human_size'],
'FileFormat': ['Format'],
'FileWidth': ['Width', 'pixel_dimension'],
'FileHeight': ['Height', 'pixel_dimension'],
'Orientation': ['Orientation', 'orientation'],
'XResolution': ['X-resolution'],
'YResolution': ['Y-resolution'],
'ResolutionUnit': ['Resolution Units', 'resolution_unit'],
}

View file

@ -1,23 +1,27 @@
from flask import Blueprint, render_template, current_app
from werkzeug.exceptions import abort
from werkzeug.utils import secure_filename
from gallery.auth import login_required
from . import db
from sqlalchemy.orm import sessionmaker
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
from . import metadata as mt
"""
Onlylegs Gallery - Routing
"""
import os
from flask import Blueprint, render_template, current_app
from werkzeug.exceptions import abort
from sqlalchemy.orm import sessionmaker
from . import db
from . import metadata as mt
blueprint = Blueprint('gallery', __name__)
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route('/')
def index():
"""
Home page of the website, shows the feed of latest images
"""
images = db_session.query(db.posts).order_by(db.posts.id.desc()).all()
return render_template('index.html',
@ -26,41 +30,45 @@ def index():
name=current_app.config['WEBSITE']['name'],
motto=current_app.config['WEBSITE']['motto'])
@blueprint.route('/image/<int:id>')
def image(id):
img = db_session.query(db.posts).filter_by(id=id).first()
@blueprint.route('/image/<int:image_id>')
def image(image_id):
"""
Image view, shows the image and its metadata
"""
img = db_session.query(db.posts).filter_by(id=image_id).first()
if img is None:
abort(404)
exif = mt.metadata.yoink(
os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name))
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
exif = mt.Metadata(img_path).yoink()
return render_template('image.html', image=img, exif=exif)
@blueprint.route('/group')
def groups():
"""
Group overview, shows all image groups
"""
return render_template('group.html', group_id='gwa gwa')
@blueprint.route('/group/<int:id>')
def group(id):
return render_template('group.html', group_id=id)
@blueprint.route('/upload')
@login_required
def upload():
return render_template('upload.html')
@blueprint.route('/group/<int:group_id>')
def group(group_id):
"""
Group view, shows all images in a group
"""
return render_template('group.html', group_id=group_id)
@blueprint.route('/profile')
def profile():
"""
Profile overview, shows all profiles on the onlylegs gallery
"""
return render_template('profile.html', user_id='gwa gwa')
@blueprint.route('/profile/<int:id>')
def profile_id(id):
return render_template('profile.html', user_id=id)
@blueprint.route('/profile/<int:user_id>')
def profile_id(user_id):
"""
Shows user ofa given id, displays their uploads and other info
"""
return render_template('profile.html', user_id=user_id)

View file

@ -1,67 +0,0 @@
import datetime
now = datetime.datetime.now()
import sys
import shutil
import os
import sass
class compile():
def __init__(self, theme, dir):
print(f"Loading '{theme}' theme...")
theme_path = os.path.join(dir, 'themes', theme)
font_path = os.path.join(dir, 'themes', theme, 'fonts')
dest = os.path.join(dir, 'static', 'theme')
# print(f"Theme path: {theme_path}")
if os.path.exists(theme_path):
if os.path.exists(os.path.join(theme_path, 'style.scss')):
theme_path = os.path.join(theme_path, 'style.scss')
elif os.path.exists(os.path.join(theme_path, 'style.sass')):
theme_path = os.path.join(theme_path, 'style.sass')
else:
print("Theme does not contain a style file!")
sys.exit(1)
self.sass = sass
self.loadTheme(theme_path, dest)
self.loadFonts(font_path, dest)
else:
print("No theme found!")
sys.exit(1)
print(f"{now.hour}:{now.minute}:{now.second} - Done!\n")
def loadTheme(self, theme, dest):
with open(os.path.join(dest, 'style.css'), 'w') as f:
try:
f.write(
self.sass.compile(filename=theme,
output_style='compressed'))
print("Compiled successfully!")
except self.sass.CompileError as e:
print("Failed to compile!\n", e)
sys.exit(1)
def loadFonts(self, source, dest):
dest = os.path.join(dest, 'fonts')
if os.path.exists(dest):
print("Updating fonts...")
try:
shutil.rmtree(dest)
except Exception as e:
print("Failed to remove old fonts!\n", e)
sys.exit(1)
try:
shutil.copytree(source, dest)
# print("Copied fonts to:", dest)
print("Copied new fonts!")
except Exception as e:
print("Failed to copy fonts!\n", e)
sys.exit(1)

View file

@ -1,31 +1,42 @@
from flask import Blueprint, render_template, url_for
from werkzeug.exceptions import abort
"""
OnlyLegs - Settings page
"""
from flask import Blueprint, render_template
from gallery.auth import login_required
from datetime import datetime
now = datetime.now()
blueprint = Blueprint('settings', __name__, url_prefix='/settings')
@blueprint.route('/')
@login_required
def general():
"""
General settings page
"""
return render_template('settings/general.html')
@blueprint.route('/server')
@login_required
def server():
"""
Server settings page
"""
return render_template('settings/server.html')
@blueprint.route('/account')
@login_required
def account():
"""
Account settings page
"""
return render_template('settings/account.html')
@blueprint.route('/logs')
@login_required
def logs():
"""
Logs settings page
"""
return render_template('settings/logs.html')

View file

@ -1,49 +1,66 @@
# Import dependencies
import platformdirs
"""
OnlyLegs - Setup
Runs when the app detects that there is no user directory
"""
import os
import sys
import platformdirs
import yaml
class setup:
def __init__(self):
self.user_dir = platformdirs.user_config_dir('onlylegs')
USER_DIR = platformdirs.user_config_dir('onlylegs')
class SetupApp:
"""
Setup the application on first run
"""
def __init__(self):
"""
Main setup function
"""
print("Running setup...")
if not os.path.exists(self.user_dir):
if not os.path.exists(USER_DIR):
self.make_dir()
if not os.path.exists(os.path.join(self.user_dir, '.env')):
if not os.path.exists(os.path.join(USER_DIR, '.env')):
self.make_env()
if not os.path.exists(os.path.join(self.user_dir, 'conf.yml')):
if not os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
self.make_yaml()
def make_dir(self):
"""
Create the user directory
"""
try:
os.makedirs(self.user_dir)
os.makedirs(os.path.join(self.user_dir, 'instance'))
os.makedirs(USER_DIR)
os.makedirs(os.path.join(USER_DIR, 'instance'))
print("Created user directory at:", self.user_dir)
except Exception as e:
print("Error creating user directory:", e)
exit(1) # exit with error code
print("Created user directory at:", USER_DIR)
except Exception as err:
print("Error creating user directory:", err)
sys.exit(1) # exit with error code
def make_env(self):
# Create .env file with default values
"""
Create the .env file with default values
"""
env_conf = {
'FLASK_SECRETE': 'dev',
}
try:
with open(os.path.join(self.user_dir, '.env'), 'w') as f:
with open(os.path.join(USER_DIR, '.env'), encoding='utf-8') as file:
for key, value in env_conf.items():
f.write(f"{key}={value}\n")
file.write(f"{key}={value}\n")
print("Created environment variables")
except Exception as e:
print("Error creating environment variables:", e)
exit(1)
except Exception as err:
print("Error creating environment variables:", err)
sys.exit(1)
print("Generated default .env file. EDIT IT BEFORE RUNNING THE APP AGAIN!")
def make_yaml(self):
# Create yaml config file with default values
"""
Create the YAML config file with default values
"""
yaml_conf = {
'admin': {
'name': 'Real Person',
@ -71,11 +88,11 @@ class setup:
},
}
try:
with open(os.path.join(self.user_dir, 'conf.yml'), 'w') as f:
yaml.dump(yaml_conf, f, default_flow_style=False)
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as file:
yaml.dump(yaml_conf, file, default_flow_style=False)
print("Created default gallery config")
except Exception as e:
print("Error creating default gallery config:", e)
exit(1)
except Exception as err:
print("Error creating default gallery config:", err)
sys.exit(1)
print("Generated default YAML config. EDIT IT BEFORE RUNNING THE APP AGAIN!")

78
gallery/theme_manager.py Normal file
View file

@ -0,0 +1,78 @@
"""
OnlyLegs - Theme Manager
"""
import os
import sys
import shutil
from datetime import datetime
import sass
class CompileTheme():
"""
Compiles the theme into the static folder
"""
def __init__(self, theme_name, app_path):
"""
Initialize the theme manager
Compiles the theme into the static folder and loads the fonts
"""
print(f"Loading '{theme_name}' theme...")
theme_path = os.path.join(app_path, 'themes', theme_name)
theme_dest = os.path.join(app_path, 'static', 'theme')
if not os.path.exists(theme_path):
print("Theme does not exist!")
sys.exit(1)
self.load_sass(theme_path, theme_dest)
self.load_fonts(theme_path, theme_dest)
now = datetime.now()
print(f"{now.hour}:{now.minute}:{now.second} - Done!\n")
def load_sass(self, source_path, css_dest):
"""
Compile the sass (or scss) file into css and save it to the static folder
"""
if os.path.join(source_path, 'style.sass'):
sass_path = os.path.join(source_path, 'style.sass')
elif os.path.join(source_path, 'style.scss'):
sass_path = os.path.join(source_path, 'style.scss')
else:
print("No sass file found!")
sys.exit(1)
with open(os.path.join(css_dest, 'style.css'), 'w', encoding='utf-8') as file:
try:
file.write(sass.compile(filename=sass_path,output_style='compressed'))
except sass.CompileError as err:
print("Failed to compile!\n", err)
sys.exit(1)
print("Compiled successfully!")
def load_fonts(self, source_path, font_dest):
"""
Copy the fonts folder to the static folder
"""
# Append fonts to the destination path
source_path = os.path.join(source_path, 'fonts')
font_dest = os.path.join(font_dest, 'fonts')
if os.path.exists(font_dest):
print("Updating fonts...")
try:
shutil.rmtree(font_dest)
except Exception as err:
print("Failed to remove old fonts!\n", err)
sys.exit(1)
try:
shutil.copytree(source_path, font_dest)
print("Copied new fonts!")
except Exception as err:
print("Failed to copy fonts!\n", err)
sys.exit(1)

297
poetry.lock generated
View file

@ -1,5 +1,25 @@
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
[[package]]
name = "astroid"
version = "2.14.2"
description = "An abstract syntax tree for Python with inference support."
category = "main"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "astroid-2.14.2-py3-none-any.whl", hash = "sha256:0e0e3709d64fbffd3037e4ff403580550f14471fd3eaae9fa11cc9a5c7901153"},
{file = "astroid-2.14.2.tar.gz", hash = "sha256:a3cf9f02c53dd259144a7e8f3ccd75d67c9a8c716ef183e0c1f291bc5d7bb3cf"},
]
[package.dependencies]
lazy-object-proxy = ">=1.4.0"
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
wrapt = [
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
{version = ">=1.14,<2", markers = "python_version >= \"3.11\""},
]
[[package]]
name = "brotli"
version = "1.0.9"
@ -134,6 +154,21 @@ files = [
[package.dependencies]
Pillow = "*"
[[package]]
name = "dill"
version = "0.3.6"
description = "serialize all of python"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"},
{file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "flask"
version = "2.2.3"
@ -148,6 +183,7 @@ files = [
[package.dependencies]
click = ">=8.0"
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
itsdangerous = ">=2.0"
Jinja2 = ">=3.0"
Werkzeug = ">=2.2.2"
@ -267,6 +303,44 @@ gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
[[package]]
name = "importlib-metadata"
version = "6.0.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"},
{file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
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 = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
category = "main"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
[package.extras]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "itsdangerous"
version = "2.1.2"
@ -297,6 +371,52 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "lazy-object-proxy"
version = "1.9.0"
description = "A fast and thorough lazy object proxy."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"},
{file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"},
{file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"},
{file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"},
{file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"},
{file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"},
{file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"},
{file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"},
{file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"},
{file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"},
{file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"},
{file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"},
{file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"},
{file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"},
{file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"},
{file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"},
{file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"},
{file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"},
{file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"},
{file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"},
{file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"},
{file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"},
{file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"},
{file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"},
{file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"},
{file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"},
{file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"},
{file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"},
{file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"},
{file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"},
{file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"},
{file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"},
{file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"},
{file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"},
{file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"},
{file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
]
[[package]]
name = "libsass"
version = "0.22.0"
@ -372,6 +492,18 @@ files = [
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "pillow"
version = "9.4.0"
@ -465,20 +597,50 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]]
name = "platformdirs"
version = "3.0.0"
version = "3.1.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
{file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
{file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
]
[package.extras]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]]
name = "pylint"
version = "2.16.3"
description = "python code static checker"
category = "main"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "pylint-2.16.3-py3-none-any.whl", hash = "sha256:3e803be66e3a34c76b0aa1a3cf4714b538335e79bd69718d34fcf36d8fff2a2b"},
{file = "pylint-2.16.3.tar.gz", hash = "sha256:0decdf8dfe30298cd9f8d82e9a1542da464db47da60e03641631086671a03621"},
]
[package.dependencies]
astroid = ">=2.14.2,<=2.16.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
{version = ">=0.3.6", markers = "python_version >= \"3.11\""},
]
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
tomlkit = ">=0.10.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]]
name = "python-dotenv"
version = "0.21.1"
@ -639,6 +801,30 @@ postgresql-psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3-binary"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "tomlkit"
version = "0.11.6"
description = "Style preserving TOML library"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
{file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
]
[[package]]
name = "typing-extensions"
version = "4.5.0"
@ -669,7 +855,108 @@ MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog"]
[[package]]
name = "wrapt"
version = "1.15.0"
description = "Module for decorators, wrappers and monkey patching."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
files = [
{file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
{file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
{file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
{file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
{file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
{file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
{file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
{file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
{file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
{file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
{file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
{file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
{file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
{file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
{file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
{file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
{file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
{file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
{file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
{file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
{file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
{file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
{file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
{file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
{file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
{file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
{file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
{file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
{file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
]
[[package]]
name = "zipp"
version = "3.15.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "8577c3b41be81184b268f983c0958e58169f3df0c179b296f3d4be40e0865737"
python-versions = "^3.9"
content-hash = "de131da70fd04213714611f747ff9102979dbc6855e68645ea93fa83a6d433d8"

View file

@ -6,9 +6,8 @@ authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT"
readme = ".github/README.md"
[tool.poetry.dependencies]
python = "^3.10"
python = "^3.8"
Flask = "^2.2.2"
flask-compress = "^1.13"
gunicorn = "^20.1.0"
@ -19,8 +18,14 @@ colorthief = "^0.2.1"
Pillow = "^9.4.0"
platformdirs = "^3.0.0"
SQLAlchemy = "^2.0.3"
pylint = "^2.16.3"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.pylint.messages_control]
# C0415: Flask uses it to register blueprints
# W1401: Anomalous backslash in string used in __init__
# W0718: Exception are logged so we don't need to raise them
disable = "C0415, W1401, W0718"