From 7ed3b455dd4e570831a1f706df455117a53bb926 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Sat, 4 Mar 2023 13:45:26 +0000 Subject: [PATCH 1/7] Submitted to PyLints needs :3 --- gallery/__init__.py | 108 ++++++++++++++++------------ gallery/api.py | 151 ++++++++++++++++++++++----------------- gallery/auth.py | 146 ++++++++++++++++++++++--------------- gallery/db.py | 113 +++++++++++++++++++---------- gallery/logger.py | 112 ----------------------------- gallery/routing.py | 74 ++++++++++--------- gallery/sassy.py | 67 ----------------- gallery/settings.py | 23 ++++-- gallery/setup.py | 85 +++++++++++++--------- gallery/theme_manager.py | 78 ++++++++++++++++++++ poetry.lock | 6 +- pyproject.toml | 6 ++ 12 files changed, 509 insertions(+), 460 deletions(-) delete mode 100644 gallery/logger.py delete mode 100644 gallery/sassy.py create mode 100644 gallery/theme_manager.py diff --git a/gallery/__init__.py b/gallery/__init__.py index 85845ca..9bad318 100644 --- a/gallery/__init__.py +++ b/gallery/__init__.py @@ -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 @@ -130,4 +146,4 @@ def create_app(test_config=None): app.register_blueprint(api.blueprint) compress.init_app(app) - return app \ No newline at end of file + return app diff --git a/gallery/api.py b/gallery/api.py index 29f56bf..0b31971 100644 --- a/gallery/api.py +++ b/gallery/api.py @@ -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/', 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,29 +55,28 @@ 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) except OSError: @@ -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/', methods=['POST']) +@blueprint.route('/remove/', 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,28 +150,32 @@ 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/', methods=['GET']) -def metadata(id): - img = db_session.query(db.posts).filter_by(id=id).first() +@blueprint.route('/metadata/', 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) @@ -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,11 +214,14 @@ 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} i += 1 # Line number, starts at 0 - return jsonify(log_dict) \ No newline at end of file + return jsonify(log_dict) diff --git a/gallery/auth.py b/gallery/auth.py index de5db08..3db96bf 100644 --- a/gallery/auth.py +++ b/gallery/auth.py @@ -1,56 +1,84 @@ +""" +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: g.user = db_session.query(db.users).filter_by(id=user_id).first() - + @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) - 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}") - flash(['Logged in successfully!', '4']) - return 'gwa gwa' - - abort(500) + logging.info('User %s logged in from %s', username, request.remote_addr) + flash(['Logged in successfully!', '4']) + return 'gwa gwa' @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 diff --git a/gallery/db.py b/gallery/db.py index 6bef906..31a43c6 100644 --- a/gallery/db.py +++ b/gallery/db.py @@ -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) @@ -19,7 +27,7 @@ class users (base): email = Column(String, unique=True, nullable=False) password = Column(String, nullable=False) created_at = Column(DateTime, nullable=False) - + posts = relationship('posts') groups = relationship('groups') session = relationship('sessions') @@ -30,19 +38,24 @@ class users (base): self.email = email 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) file_name = Column(String, unique=True, nullable=False) description = Column(String, nullable=False) alt = Column(String, nullable=False) author_id = Column(Integer, ForeignKey('users.id')) created_at = Column(DateTime, nullable=False) - + junction = relationship('group_junction') - + def __init__(self, file_name, description, alt, author_id): self.file_name = file_name self.description = description @@ -50,84 +63,108 @@ 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) name = Column(String, nullable=False) description = Column(String, nullable=False) author_id = Column(Integer, ForeignKey('users.id')) created_at = Column(DateTime, nullable=False) - + junction = relationship('group_junction') - + def __init__(self, name, description, author_id): self.name = name self.description = description self.author_id = author_id self.created_at = datetime.now() - -class group_junction (base): + + +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) group_id = Column(Integer, ForeignKey('groups.id')) post_id = Column(Integer, ForeignKey('posts.id')) - + def __init__(self, group_id, post_id): self.group_id = group_id self.post_id = post_id - -class sessions (base): + + +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() -base.metadata.create_all(engine) \ No newline at end of file +base.metadata.create_all(engine) diff --git a/gallery/logger.py b/gallery/logger.py deleted file mode 100644 index 188fff8..0000000 --- a/gallery/logger.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/gallery/routing.py b/gallery/routing.py index 88b2f98..0388786 100644 --- a/gallery/routing.py +++ b/gallery/routing.py @@ -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,10 +30,12 @@ def index(): name=current_app.config['WEBSITE']['name'], motto=current_app.config['WEBSITE']['motto']) - -@blueprint.route('/image/') -def image(id): - img = db_session.query(db.posts).filter_by(id=id).first() +@blueprint.route('/image/') +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) @@ -39,28 +45,30 @@ def image(id): 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/') -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/') +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/') -def profile_id(id): - return render_template('profile.html', user_id=id) \ No newline at end of file +@blueprint.route('/profile/') +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) diff --git a/gallery/sassy.py b/gallery/sassy.py deleted file mode 100644 index 8dc3603..0000000 --- a/gallery/sassy.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/gallery/settings.py b/gallery/settings.py index 9d01c1a..a4cd845 100644 --- a/gallery/settings.py +++ b/gallery/settings.py @@ -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(): - return render_template('settings/logs.html') \ No newline at end of file + """ + Logs settings page + """ + return render_template('settings/logs.html') diff --git a/gallery/setup.py b/gallery/setup.py index 397419e..fea6547 100644 --- a/gallery/setup.py +++ b/gallery/setup.py @@ -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')) - - print("Created user directory at:", self.user_dir) - except Exception as e: - print("Error creating user directory:", e) - exit(1) # exit with error code - + os.makedirs(USER_DIR) + os.makedirs(os.path.join(USER_DIR, 'instance')) + + 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) - - print("Generated default YAML config. EDIT IT BEFORE RUNNING THE APP AGAIN!") \ No newline at end of file + 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!") diff --git a/gallery/theme_manager.py b/gallery/theme_manager.py new file mode 100644 index 0000000..72b075c --- /dev/null +++ b/gallery/theme_manager.py @@ -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'), 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) diff --git a/poetry.lock b/poetry.lock index 0a09bdd..d8550a8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -465,14 +465,14 @@ 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] diff --git a/pyproject.toml b/pyproject.toml index ba42012..aee48ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,3 +24,9 @@ SQLAlchemy = "^2.0.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" From de2d72e5dedc5b044bc9b1539fabe19ec2e81888 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Sat, 4 Mar 2023 21:08:42 +0000 Subject: [PATCH 2/7] Submitted to PyLint --- gallery/api.py | 4 +- gallery/metadata.py | 705 ----------------------------------- gallery/metadata/__init__.py | 117 ++++++ gallery/metadata/helpers.py | 407 ++++++++++++++++++++ gallery/metadata/mapping.py | 62 +++ gallery/routing.py | 4 +- gallery/theme_manager.py | 2 +- 7 files changed, 591 insertions(+), 710 deletions(-) delete mode 100644 gallery/metadata.py create mode 100644 gallery/metadata/__init__.py create mode 100644 gallery/metadata/helpers.py create mode 100644 gallery/metadata/mapping.py diff --git a/gallery/api.py b/gallery/api.py index 0b31971..04533d9 100644 --- a/gallery/api.py +++ b/gallery/api.py @@ -180,8 +180,8 @@ def metadata(img_id): 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) diff --git a/gallery/metadata.py b/gallery/metadata.py deleted file mode 100644 index 22af64e..0000000 --- a/gallery/metadata.py +++ /dev/null @@ -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) + '%' \ No newline at end of file diff --git a/gallery/metadata/__init__.py b/gallery/metadata/__init__.py new file mode 100644 index 0000000..8bb4e6e --- /dev/null +++ b/gallery/metadata/__init__.py @@ -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 diff --git a/gallery/metadata/helpers.py b/gallery/metadata/helpers.py new file mode 100644 index 0000000..5dc4703 --- /dev/null +++ b/gallery/metadata/helpers.py @@ -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' diff --git a/gallery/metadata/mapping.py b/gallery/metadata/mapping.py new file mode 100644 index 0000000..d99c1df --- /dev/null +++ b/gallery/metadata/mapping.py @@ -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'], +} diff --git a/gallery/routing.py b/gallery/routing.py index 0388786..9389bcf 100644 --- a/gallery/routing.py +++ b/gallery/routing.py @@ -40,8 +40,8 @@ def image(image_id): 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) diff --git a/gallery/theme_manager.py b/gallery/theme_manager.py index 72b075c..73c5702 100644 --- a/gallery/theme_manager.py +++ b/gallery/theme_manager.py @@ -45,7 +45,7 @@ class CompileTheme(): print("No sass file found!") sys.exit(1) - with open(os.path.join(css_dest, 'style.css'), encoding='utf-8') as file: + 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: From a16c1b2da93b5bf5e0505727f4a0aaa78fb84294 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Sat, 4 Mar 2023 21:15:53 +0000 Subject: [PATCH 3/7] Fix workflow, I think --- .github/workflows/pylint.yml | 5 +- poetry.lock | 251 ++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 254 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 383e65c..41df19b 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -17,7 +17,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint + pip install poetry + python -m poetry install - name: Analysing the code with pylint run: | - pylint $(git ls-files '*.py') + python -m poetry run -m pylint $(git ls-files '*.py') diff --git a/poetry.lock b/poetry.lock index d8550a8..765b866 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" @@ -267,6 +302,24 @@ gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] tornado = ["tornado (>=0.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 +350,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 +471,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" @@ -479,6 +590,35 @@ files = [ 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" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + [[package]] name = "python-dotenv" version = "0.21.1" @@ -639,6 +779,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 +833,92 @@ 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"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8577c3b41be81184b268f983c0958e58169f3df0c179b296f3d4be40e0865737" +content-hash = "97f6d1b563cbbb67ea84bf33bde6392457edd4724a0578c4702bbe229f0c7f0d" diff --git a/pyproject.toml b/pyproject.toml index aee48ed..75f8931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ colorthief = "^0.2.1" Pillow = "^9.4.0" platformdirs = "^3.0.0" SQLAlchemy = "^2.0.3" +pylint = "^2.16.3" [build-system] From 2ad18a89285eba077cccac283c7bb48d9381f8a7 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Sat, 4 Mar 2023 21:21:06 +0000 Subject: [PATCH 4/7] woopsie --- poetry.lock | 42 ++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 4 +--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 765b866..4c53cf6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -183,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" @@ -302,6 +303,26 @@ 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" @@ -614,6 +635,7 @@ 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)"] @@ -918,7 +940,23 @@ files = [ {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 = "97f6d1b563cbbb67ea84bf33bde6392457edd4724a0578c4702bbe229f0c7f0d" +python-versions = "^3.9" +content-hash = "de131da70fd04213714611f747ff9102979dbc6855e68645ea93fa83a6d433d8" diff --git a/pyproject.toml b/pyproject.toml index 75f8931..ec85bac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,8 @@ authors = ["Fluffy-Bean "] license = "MIT" readme = ".github/README.md" - [tool.poetry.dependencies] -python = "^3.10" +python = "^3.9" Flask = "^2.2.2" flask-compress = "^1.13" gunicorn = "^20.1.0" @@ -21,7 +20,6 @@ platformdirs = "^3.0.0" SQLAlchemy = "^2.0.3" pylint = "^2.16.3" - [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" From b4ee8dfe7f63e447c4308fce248715543f54514b Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Sat, 4 Mar 2023 21:23:07 +0000 Subject: [PATCH 5/7] This is a terrible day --- .github/workflows/pylint.yml | 5 ++--- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 41df19b..03ff7b2 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -16,9 +16,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install poetry + python -m pip install --upgrade pip poetry python -m poetry install - name: Analysing the code with pylint run: | - python -m poetry run -m pylint $(git ls-files '*.py') + poetry run -m pylint $(git ls-files '*.py') diff --git a/pyproject.toml b/pyproject.toml index ec85bac..7d7a013 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" readme = ".github/README.md" [tool.poetry.dependencies] -python = "^3.9" +python = "^3.8" Flask = "^2.2.2" flask-compress = "^1.13" gunicorn = "^20.1.0" From c98a524c08ed2f09724ed0d64e4d415e7019aca7 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Sat, 4 Mar 2023 21:24:43 +0000 Subject: [PATCH 6/7] :3 --- .github/workflows/pylint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 03ff7b2..c2ebb9d 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -20,4 +20,4 @@ jobs: python -m poetry install - name: Analysing the code with pylint run: | - poetry run -m pylint $(git ls-files '*.py') + poetry run python3 -m pylint $(git ls-files '*.py') From 970d3dcf667afe730e75148d5273950ade83d096 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Sat, 4 Mar 2023 21:46:19 +0000 Subject: [PATCH 7/7] Switch workflow to run on pull request to main Update README --- .github/workflows/pylint.yml | 4 +++- README.md | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index c2ebb9d..1744bbf 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -1,6 +1,8 @@ name: Pylint -on: [push] +on: + pull_request: + branches: [ main ] jobs: build: diff --git a/README.md b/README.md index d3afea7..fbd8d4f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ + + wakatime + ## Features