mirror of
https://github.com/Derpy-Leggies/OnlyLegs.git
synced 2025-06-29 03:26:16 +00:00
commit
7827c23402
18 changed files with 1399 additions and 1178 deletions
10
.github/workflows/pylint.yml
vendored
10
.github/workflows/pylint.yml
vendored
|
@ -1,6 +1,8 @@
|
||||||
name: Pylint
|
name: Pylint
|
||||||
|
|
||||||
on: [push]
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -16,8 +18,8 @@ jobs:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip poetry
|
||||||
pip install pylint
|
python -m poetry install
|
||||||
- name: Analysing the code with pylint
|
- name: Analysing the code with pylint
|
||||||
run: |
|
run: |
|
||||||
pylint $(git ls-files '*.py')
|
poetry run python3 -m pylint $(git ls-files '*.py')
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
<a href="https://github.com/Fluffy-Bean/onlylegs/blob/main/LICENSE">
|
<a href="https://github.com/Fluffy-Bean/onlylegs/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/github/license/Fluffy-Bean/onlylegs?style=for-the-badge">
|
<img src="https://img.shields.io/github/license/Fluffy-Bean/onlylegs?style=for-the-badge">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://wakatime.com/badge/user/29bd1733-45f0-41c0-901e-d6daf49094d4/project/6aae41df-003f-4b17-ae8f-62cecfb3fc24">
|
||||||
|
<img src="https://wakatime.com/badge/user/29bd1733-45f0-41c0-901e-d6daf49094d4/project/6aae41df-003f-4b17-ae8f-62cecfb3fc24.svg?style=for-the-badge" alt="wakatime">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
|
@ -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_compress import Compress
|
||||||
|
from flask import Flask, render_template
|
||||||
|
|
||||||
|
# Configuration
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import platformdirs
|
import platformdirs
|
||||||
|
|
||||||
from gallery.logger import logger
|
|
||||||
logger.innit_logger()
|
|
||||||
|
|
||||||
import yaml
|
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
|
# Check if any of the required files are missing
|
||||||
if not os.path.exists(platformdirs.user_config_dir('onlylegs')):
|
if not os.path.exists(platformdirs.user_config_dir('onlylegs')):
|
||||||
from .setup import setup
|
from . import setup
|
||||||
setup()
|
setup.SetupApp()
|
||||||
|
|
||||||
|
|
||||||
user_dir = platformdirs.user_config_dir('onlylegs')
|
|
||||||
instance_path = os.path.join(user_dir, 'instance')
|
|
||||||
|
|
||||||
# Get environment variables
|
# Get environment variables
|
||||||
if os.path.exists(os.path.join(user_dir, '.env')):
|
if os.path.exists(os.path.join(USER_DIR, '.env')):
|
||||||
load_dotenv(os.path.join(user_dir, '.env'))
|
load_dotenv(os.path.join(USER_DIR, '.env'))
|
||||||
print("Loaded environment variables")
|
print("Loaded environment variables")
|
||||||
else:
|
else:
|
||||||
print("No environment variables found!")
|
print("No environment variables found!")
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
# Get config file
|
# Get config file
|
||||||
if os.path.exists(os.path.join(user_dir, 'conf.yml')):
|
if os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
|
||||||
with open(os.path.join(user_dir, 'conf.yml'), 'r') as f:
|
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as f:
|
||||||
conf = yaml.load(f, Loader=yaml.FullLoader)
|
conf = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
print("Loaded gallery config")
|
print("Loaded gallery config")
|
||||||
else:
|
else:
|
||||||
print("No config file found!")
|
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):
|
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()
|
compress = Compress()
|
||||||
|
|
||||||
# App configuration
|
# App configuration
|
||||||
app.config.from_mapping(
|
app.config.from_mapping(
|
||||||
SECRET_KEY=os.environ.get('FLASK_SECRET'),
|
SECRET_KEY=os.environ.get('FLASK_SECRET'),
|
||||||
DATABASE=os.path.join(app.instance_path, 'gallery.sqlite'),
|
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'],
|
ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'],
|
||||||
MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'],
|
MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'],
|
||||||
WEBSITE=conf['website'],
|
WEBSITE=conf['website'],
|
||||||
)
|
)
|
||||||
|
|
||||||
if test_config is None:
|
if test_config is None:
|
||||||
# load the instance config, if it exists, when not testing
|
|
||||||
app.config.from_pyfile('config.py', silent=True)
|
app.config.from_pyfile('config.py', silent=True)
|
||||||
else:
|
else:
|
||||||
# load the test config if passed in
|
|
||||||
app.config.from_mapping(test_config)
|
app.config.from_mapping(test_config)
|
||||||
|
|
||||||
# ensure the instance folder exists
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(app.instance_path)
|
os.makedirs(app.instance_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Load theme
|
theme_manager.CompileTheme('default', app.root_path)
|
||||||
from . import sassy
|
|
||||||
sassy.compile('default', app.root_path)
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(405)
|
@app.errorhandler(405)
|
||||||
def method_not_allowed(e):
|
def method_not_allowed(err):
|
||||||
error = '405'
|
error = '405'
|
||||||
msg = e.description
|
msg = err.description
|
||||||
return render_template('error.html', error=error, msg=e), 404
|
return render_template('error.html', error=error, msg=msg), 404
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
def page_not_found(err):
|
||||||
error = '404'
|
error = '404'
|
||||||
msg = e.description
|
msg = err.description
|
||||||
return render_template('error.html', error=error, msg=msg), 404
|
return render_template('error.html', error=error, msg=msg), 404
|
||||||
|
|
||||||
@app.errorhandler(403)
|
@app.errorhandler(403)
|
||||||
def forbidden(e):
|
def forbidden(err):
|
||||||
error = '403'
|
error = '403'
|
||||||
msg = e.description
|
msg = err.description
|
||||||
return render_template('error.html', error=error, msg=msg), 403
|
return render_template('error.html', error=error, msg=msg), 403
|
||||||
|
|
||||||
@app.errorhandler(410)
|
@app.errorhandler(410)
|
||||||
def gone(e):
|
def gone(err):
|
||||||
error = '410'
|
error = '410'
|
||||||
msg = e.description
|
msg = err.description
|
||||||
return render_template('error.html', error=error, msg=msg), 410
|
return render_template('error.html', error=error, msg=msg), 410
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def internal_server_error(e):
|
def internal_server_error(err):
|
||||||
error = '500'
|
error = '500'
|
||||||
msg = e.description
|
msg = err.description
|
||||||
return render_template('error.html', error=error, msg=msg), 500
|
return render_template('error.html', error=error, msg=msg), 500
|
||||||
|
|
||||||
# Load login, registration and logout manager
|
# Load login, registration and logout manager
|
||||||
|
@ -130,4 +146,4 @@ def create_app(test_config=None):
|
||||||
app.register_blueprint(api.blueprint)
|
app.register_blueprint(api.blueprint)
|
||||||
|
|
||||||
compress.init_app(app)
|
compress.init_app(app)
|
||||||
return app
|
return app
|
||||||
|
|
155
gallery/api.py
155
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 werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from PIL import Image, ImageOps # ImageFilter
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from gallery.auth import login_required
|
from gallery.auth import login_required
|
||||||
|
|
||||||
from . import db
|
from . import db # Import db to create a session
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
db_session = sessionmaker(bind=db.engine)
|
|
||||||
db_session = db_session()
|
|
||||||
|
|
||||||
from PIL import Image, ImageOps, ImageFilter
|
|
||||||
from . import metadata as mt
|
from . import metadata as mt
|
||||||
|
|
||||||
from .logger import logger
|
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
|
|
||||||
blueprint = Blueprint('api', __name__, url_prefix='/api')
|
blueprint = Blueprint('api', __name__, url_prefix='/api')
|
||||||
|
db_session = sessionmaker(bind=db.engine)
|
||||||
|
db_session = db_session()
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/uploads/<file>', methods=['GET'])
|
@blueprint.route('/uploads/<file>', methods=['GET'])
|
||||||
def uploads(file):
|
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
|
# Get args
|
||||||
width = request.args.get('w', default=0, type=int) # Width of image
|
width = request.args.get('w', default=0, type=int) # Width of image
|
||||||
height = request.args.get('h', default=0, type=int) # Height 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,
|
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters
|
||||||
# such as blur for NSFW images
|
|
||||||
|
|
||||||
# if no args are passed, return the raw file
|
# if no args are passed, return the raw file
|
||||||
if width == 0 and height == 0 and not filtered:
|
if width == 0 and height == 0 and not filtered:
|
||||||
return send_from_directory(current_app.config['UPLOAD_FOLDER'],
|
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||||
secure_filename(file),
|
secure_filename(file))):
|
||||||
as_attachment=True)
|
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
|
# Of either width or height is 0, set it to the other value to keep aspect ratio
|
||||||
if width > 0 and height == 0:
|
if width > 0 and height == 0:
|
||||||
|
@ -45,29 +55,28 @@ def uploads(file):
|
||||||
|
|
||||||
# Open image and set extension
|
# Open image and set extension
|
||||||
try:
|
try:
|
||||||
img = Image.open(
|
img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'],file))
|
||||||
os.path.join(current_app.config['UPLOAD_FOLDER'],
|
except FileNotFoundError:
|
||||||
secure_filename(file)))
|
logging.error('File not found: %s, possibly broken upload', file)
|
||||||
except Exception as e:
|
abort(404)
|
||||||
logger.server(600, f"Error opening image: {e}")
|
except Exception as err:
|
||||||
|
logging.error('Error opening image: %s', err)
|
||||||
abort(500)
|
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_ext = set_ext[img_ext]
|
||||||
img_icc = img.info.get(
|
# Get ICC profile as it alters colours when saving
|
||||||
"icc_profile") # Get ICC profile as it alters colours
|
img_icc = img.info.get("icc_profile")
|
||||||
|
|
||||||
# Resize image and orientate correctly
|
# Resize image and orientate correctly
|
||||||
img.thumbnail((width, height), Image.LANCZOS)
|
img.thumbnail((width, height), Image.LANCZOS)
|
||||||
img = ImageOps.exif_transpose(img)
|
img = ImageOps.exif_transpose(img)
|
||||||
|
|
||||||
# TODO: Add filters
|
|
||||||
# If has NSFW tag, blur image, etc.
|
# If has NSFW tag, blur image, etc.
|
||||||
if filtered:
|
if filtered:
|
||||||
#pass
|
#img = img.filter(ImageFilter.GaussianBlur(20))
|
||||||
img = img.filter(ImageFilter.GaussianBlur(20))
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
img.save(buff, img_ext, icc_profile=img_icc)
|
img.save(buff, img_ext, icc_profile=img_icc)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
@ -75,8 +84,8 @@ def uploads(file):
|
||||||
# Convert to RGB and try again
|
# Convert to RGB and try again
|
||||||
img = img.convert('RGB')
|
img = img.convert('RGB')
|
||||||
img.save(buff, img_ext, icc_profile=img_icc)
|
img.save(buff, img_ext, icc_profile=img_icc)
|
||||||
except:
|
except Exception as err:
|
||||||
logger.server(600, f"Error resizing image: {file}")
|
logging.error('Could not resize image %s, error: %s', file, err)
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
img.close()
|
img.close()
|
||||||
|
@ -89,47 +98,51 @@ def uploads(file):
|
||||||
@blueprint.route('/upload', methods=['POST'])
|
@blueprint.route('/upload', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def upload():
|
def upload():
|
||||||
|
"""
|
||||||
|
Uploads an image to the server and saves it to the database
|
||||||
|
"""
|
||||||
form_file = request.files['file']
|
form_file = request.files['file']
|
||||||
form = request.form
|
form = request.form
|
||||||
|
|
||||||
if not form_file:
|
if not form_file:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
|
||||||
img_ext = os.path.splitext(secure_filename(
|
img_ext = os.path.splitext(form_file.filename)[-1].replace('.', '').lower()
|
||||||
form_file.filename))[-1].replace('.', '').lower()
|
img_name = f"GWAGWA_{str(uuid4())}.{img_ext}"
|
||||||
img_name = f"GWAGWA_{uuid4().__str__()}.{img_ext}"
|
|
||||||
|
|
||||||
if not img_ext in current_app.config['ALLOWED_EXTENSIONS'].keys():
|
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)
|
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'])
|
os.mkdir(current_app.config['UPLOAD_FOLDER'])
|
||||||
|
|
||||||
# Save to database
|
# Save to database
|
||||||
try:
|
try:
|
||||||
tr = db.posts(img_name, form['description'], form['alt'], g.user.id)
|
db_session.add(db.posts(img_name, form['description'], form['alt'], g.user.id))
|
||||||
db_session.add(tr)
|
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
logger.server(600, f"Error saving to database: {e}")
|
logging.error('Could not save to database: %s', err)
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
# Save file
|
# Save file
|
||||||
try:
|
try:
|
||||||
form_file.save(
|
form_file.save(
|
||||||
os.path.join(current_app.config['UPLOAD_FOLDER'], img_name))
|
os.path.join(current_app.config['UPLOAD_FOLDER'], img_name))
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
logger.server(600, f"Error saving file: {e}")
|
logging.error('Could not save file: %s', err)
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
return 'Gwa Gwa'
|
return 'Gwa Gwa'
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/remove/<int:id>', methods=['POST'])
|
@blueprint.route('/remove/<int:img_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def remove(id):
|
def remove(img_id):
|
||||||
img = db_session.query(db.posts).filter_by(id=id).first()
|
"""
|
||||||
|
Deletes an image from the server and database
|
||||||
|
"""
|
||||||
|
img = db_session.query(db.posts).filter_by(id=img_id).first()
|
||||||
|
|
||||||
if img is None:
|
if img is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@ -137,34 +150,38 @@ def remove(id):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.remove(
|
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
|
||||||
os.path.join(current_app.config['UPLOAD_FOLDER'],
|
except FileNotFoundError:
|
||||||
img.file_name))
|
# File was already deleted or doesn't exist
|
||||||
except Exception as e:
|
logging.warning('File not found: %s, already deleted or never existed', img.file_name)
|
||||||
logger.server(600, f"Error removing file: {e}")
|
except Exception as err:
|
||||||
|
logging.error('Could not remove file: %s', err)
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
try:
|
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()
|
db_session.commit()
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
logger.server(600, f"Error removing from database: {e}")
|
logging.error('Could not remove from database: %s', err)
|
||||||
abort(500)
|
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])
|
flash(['Image was all in Le Head!', 1])
|
||||||
return 'Gwa Gwa'
|
return 'Gwa Gwa'
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/metadata/<int:id>', methods=['GET'])
|
@blueprint.route('/metadata/<int:img_id>', methods=['GET'])
|
||||||
def metadata(id):
|
def metadata(img_id):
|
||||||
img = db_session.query(db.posts).filter_by(id=id).first()
|
"""
|
||||||
|
Yoinks metadata from an image
|
||||||
|
"""
|
||||||
|
img = db_session.query(db.posts).filter_by(id=img_id).first()
|
||||||
|
|
||||||
if img is None:
|
if img is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
exif = mt.metadata.yoink(
|
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
|
||||||
os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name))
|
exif = mt.Metadata(img_path).yoink()
|
||||||
|
|
||||||
return jsonify(exif)
|
return jsonify(exif)
|
||||||
|
|
||||||
|
@ -172,12 +189,15 @@ def metadata(id):
|
||||||
@blueprint.route('/logfile')
|
@blueprint.route('/logfile')
|
||||||
@login_required
|
@login_required
|
||||||
def logfile():
|
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 = {}
|
log_dict = {}
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
with open(filename) as f:
|
with open(filename, encoding='utf-8') as file:
|
||||||
for line in f:
|
for line in file:
|
||||||
line = line.split(' : ')
|
line = line.split(' : ')
|
||||||
|
|
||||||
event = line[0].strip().split(' ')
|
event = line[0].strip().split(' ')
|
||||||
|
@ -194,11 +214,14 @@ def logfile():
|
||||||
'code': int(message[1:4]),
|
'code': int(message[1:4]),
|
||||||
'message': message[5:].strip()
|
'message': message[5:].strip()
|
||||||
}
|
}
|
||||||
except:
|
except ValueError:
|
||||||
message_data = {'code': 0, 'message': message}
|
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}
|
log_dict[i] = {'event': event_data, 'message': message_data}
|
||||||
|
|
||||||
i += 1 # Line number, starts at 0
|
i += 1 # Line number, starts at 0
|
||||||
|
|
||||||
return jsonify(log_dict)
|
return jsonify(log_dict)
|
||||||
|
|
146
gallery/auth.py
146
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
|
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 werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from gallery import db
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
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 = sessionmaker(bind=db.engine)
|
||||||
db_session = db_session()
|
db_session = db_session()
|
||||||
|
|
||||||
from .logger import logger
|
|
||||||
|
|
||||||
import re
|
def login_required(view):
|
||||||
import uuid
|
"""
|
||||||
|
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
|
@blueprint.before_app_request
|
||||||
def load_logged_in_user():
|
def load_logged_in_user():
|
||||||
|
"""
|
||||||
|
Runs before every request and checks if a user is logged in
|
||||||
|
"""
|
||||||
user_id = session.get('user_id')
|
user_id = session.get('user_id')
|
||||||
user_uuid = session.get('uuid')
|
user_uuid = session.get('uuid')
|
||||||
|
|
||||||
if user_id is None or user_uuid is None:
|
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
|
g.user = None
|
||||||
session.clear()
|
session.clear()
|
||||||
else:
|
else:
|
||||||
is_alive = db_session.query(db.sessions).filter_by(session_uuid=user_uuid).first()
|
is_alive = db_session.query(db.sessions).filter_by(session_uuid=user_uuid).first()
|
||||||
|
|
||||||
if is_alive is None:
|
if is_alive is None:
|
||||||
logger.add(103, 'Session expired')
|
logging.info('Session expired')
|
||||||
flash(['Session expired!', '3'])
|
flash(['Session expired!', '3'])
|
||||||
session.clear()
|
session.clear()
|
||||||
else:
|
else:
|
||||||
g.user = db_session.query(db.users).filter_by(id=user_id).first()
|
g.user = db_session.query(db.users).filter_by(id=user_id).first()
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/register', methods=['POST'])
|
@blueprint.route('/register', methods=['POST'])
|
||||||
def register():
|
def register():
|
||||||
|
"""
|
||||||
|
Register a new user
|
||||||
|
"""
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
email = request.form['email']
|
email = request.form['email']
|
||||||
password = request.form['password']
|
password = request.form['password']
|
||||||
password_repeat = request.form['password-repeat']
|
password_repeat = request.form['password-repeat']
|
||||||
|
|
||||||
error = []
|
error = []
|
||||||
|
|
||||||
if not username:
|
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
|
||||||
error.append('Username is empty!')
|
username_regex = re.compile(r'\b[A-Za-z0-9._%+-]+\b')
|
||||||
|
|
||||||
if not email:
|
|
||||||
error.append('Email is empty!')
|
if not username or not username_regex.match(username):
|
||||||
elif not re.match(
|
error.append('Username is invalid!')
|
||||||
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email):
|
|
||||||
|
if not email or not email_regex.match(email):
|
||||||
error.append('Email is invalid!')
|
error.append('Email is invalid!')
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
|
@ -59,73 +87,77 @@ def register():
|
||||||
error.append('Password is too short! Longer than 8 characters pls')
|
error.append('Password is too short! Longer than 8 characters pls')
|
||||||
|
|
||||||
if not password_repeat:
|
if not password_repeat:
|
||||||
error.append('Password repeat is empty!')
|
error.append('Enter password again!')
|
||||||
elif password_repeat != password:
|
elif password_repeat != password:
|
||||||
error.append('Passwords do not match!')
|
error.append('Passwords do not match!')
|
||||||
|
|
||||||
if not error:
|
if error:
|
||||||
try:
|
return jsonify(error)
|
||||||
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'
|
|
||||||
|
|
||||||
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'])
|
@blueprint.route('/login', methods=['POST'])
|
||||||
def login():
|
def login():
|
||||||
|
"""
|
||||||
|
Log in a registered user by adding the user id to the session
|
||||||
|
"""
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
password = request.form['password']
|
password = request.form['password']
|
||||||
error = None
|
|
||||||
user = db_session.query(db.users).filter_by(username=username).first()
|
user = db_session.query(db.users).filter_by(username=username).first()
|
||||||
|
error = []
|
||||||
|
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
logger.add(101, f"User {username} does not exist from {request.remote_addr}")
|
logging.error('User %s does not exist. Login attempt from %s',
|
||||||
abort(403)
|
username, request.remote_addr)
|
||||||
|
error.append('Username or Password is incorrect!')
|
||||||
elif not check_password_hash(user.password, password):
|
elif not check_password_hash(user.password, password):
|
||||||
logger.add(102, f"User {username} password error from {request.remote_addr}")
|
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)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session.clear()
|
session.clear()
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['uuid'] = str(uuid.uuid4())
|
session['uuid'] = str(uuid.uuid4())
|
||||||
|
|
||||||
tr = db.sessions(user.id, session.get('uuid'), request.remote_addr, request.user_agent.string, 1)
|
db_session.add(db.sessions(user.id,
|
||||||
db_session.add(tr)
|
session.get('uuid'),
|
||||||
|
request.remote_addr,
|
||||||
|
request.user_agent.string,
|
||||||
|
1))
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
except error as err:
|
except Exception as err:
|
||||||
logger.add(105, f"User {username} auth error: {err}")
|
logging.error('User %s could not be logged in: %s', username, err)
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
if error is None:
|
logging.info('User %s logged in from %s', username, request.remote_addr)
|
||||||
logger.add(100, f"User {username} logged in from {request.remote_addr}")
|
flash(['Logged in successfully!', '4'])
|
||||||
flash(['Logged in successfully!', '4'])
|
return 'gwa gwa'
|
||||||
return 'gwa gwa'
|
|
||||||
|
|
||||||
abort(500)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/logout')
|
@blueprint.route('/logout')
|
||||||
def 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()
|
session.clear()
|
||||||
return redirect(url_for('index'))
|
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
|
|
||||||
|
|
113
gallery/db.py
113
gallery/db.py
|
@ -1,6 +1,10 @@
|
||||||
|
"""
|
||||||
|
OnlyLegs - Database
|
||||||
|
Database models and functions for SQLAlchemy
|
||||||
|
"""
|
||||||
import os
|
import os
|
||||||
import platformdirs
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import platformdirs
|
||||||
|
|
||||||
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||||
from sqlalchemy.orm import declarative_base, relationship
|
from sqlalchemy.orm import declarative_base, relationship
|
||||||
|
@ -11,7 +15,11 @@ engine = create_engine(f'sqlite:///{path_to_db}', echo=False)
|
||||||
base = declarative_base()
|
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'
|
__tablename__ = 'users'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
@ -19,7 +27,7 @@ class users (base):
|
||||||
email = Column(String, unique=True, nullable=False)
|
email = Column(String, unique=True, nullable=False)
|
||||||
password = Column(String, nullable=False)
|
password = Column(String, nullable=False)
|
||||||
created_at = Column(DateTime, nullable=False)
|
created_at = Column(DateTime, nullable=False)
|
||||||
|
|
||||||
posts = relationship('posts')
|
posts = relationship('posts')
|
||||||
groups = relationship('groups')
|
groups = relationship('groups')
|
||||||
session = relationship('sessions')
|
session = relationship('sessions')
|
||||||
|
@ -30,19 +38,24 @@ class users (base):
|
||||||
self.email = email
|
self.email = email
|
||||||
self.password = password
|
self.password = password
|
||||||
self.created_at = datetime.now()
|
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'
|
__tablename__ = 'posts'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
file_name = Column(String, unique=True, nullable=False)
|
file_name = Column(String, unique=True, nullable=False)
|
||||||
description = Column(String, nullable=False)
|
description = Column(String, nullable=False)
|
||||||
alt = Column(String, nullable=False)
|
alt = Column(String, nullable=False)
|
||||||
author_id = Column(Integer, ForeignKey('users.id'))
|
author_id = Column(Integer, ForeignKey('users.id'))
|
||||||
created_at = Column(DateTime, nullable=False)
|
created_at = Column(DateTime, nullable=False)
|
||||||
|
|
||||||
junction = relationship('group_junction')
|
junction = relationship('group_junction')
|
||||||
|
|
||||||
def __init__(self, file_name, description, alt, author_id):
|
def __init__(self, file_name, description, alt, author_id):
|
||||||
self.file_name = file_name
|
self.file_name = file_name
|
||||||
self.description = description
|
self.description = description
|
||||||
|
@ -50,84 +63,108 @@ class posts (base):
|
||||||
self.author_id = author_id
|
self.author_id = author_id
|
||||||
self.created_at = datetime.now()
|
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'
|
__tablename__ = 'groups'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
description = Column(String, nullable=False)
|
description = Column(String, nullable=False)
|
||||||
author_id = Column(Integer, ForeignKey('users.id'))
|
author_id = Column(Integer, ForeignKey('users.id'))
|
||||||
created_at = Column(DateTime, nullable=False)
|
created_at = Column(DateTime, nullable=False)
|
||||||
|
|
||||||
junction = relationship('group_junction')
|
junction = relationship('group_junction')
|
||||||
|
|
||||||
def __init__(self, name, description, author_id):
|
def __init__(self, name, description, author_id):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.author_id = author_id
|
self.author_id = author_id
|
||||||
self.created_at = datetime.now()
|
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'
|
__tablename__ = 'group_junction'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
group_id = Column(Integer, ForeignKey('groups.id'))
|
group_id = Column(Integer, ForeignKey('groups.id'))
|
||||||
post_id = Column(Integer, ForeignKey('posts.id'))
|
post_id = Column(Integer, ForeignKey('posts.id'))
|
||||||
|
|
||||||
def __init__(self, group_id, post_id):
|
def __init__(self, group_id, post_id):
|
||||||
self.group_id = group_id
|
self.group_id = group_id
|
||||||
self.post_id = post_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'
|
__tablename__ = 'sessions'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
user_id = Column(Integer, ForeignKey('users.id'))
|
user_id = Column(Integer, ForeignKey('users.id'))
|
||||||
session_uuid = Column(String, nullable=False)
|
session_uuid = Column(String, nullable=False)
|
||||||
ip = Column(String, nullable=False)
|
ip_address = Column(String, nullable=False)
|
||||||
user_agent = Column(String, nullable=False)
|
user_agent = Column(String, nullable=False)
|
||||||
active = Column(Boolean, nullable=False)
|
active = Column(Boolean, nullable=False)
|
||||||
created_at = Column(DateTime, 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.user_id = user_id
|
||||||
self.session_uuid = session_uuid
|
self.session_uuid = session_uuid
|
||||||
self.ip = ip
|
self.ip_address = ip_address
|
||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
self.active = active
|
self.active = active
|
||||||
self.created_at = datetime.now()
|
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'
|
__tablename__ = 'logs'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
user_id = Column(Integer, ForeignKey('users.id'))
|
user_id = Column(Integer, ForeignKey('users.id'))
|
||||||
ip = Column(String, nullable=False)
|
ip_address = Column(String, nullable=False)
|
||||||
code = Column(Integer, nullable=False)
|
code = Column(Integer, nullable=False)
|
||||||
msg = Column(String, nullable=False)
|
msg = Column(String, nullable=False)
|
||||||
created_at = Column(DateTime, 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.user_id = user_id
|
||||||
self.ip = ip
|
self.ip_address = ip_address
|
||||||
self.code = code
|
self.code = code
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.created_at = datetime.now()
|
self.created_at = datetime.now()
|
||||||
|
|
||||||
class bans (base):
|
|
||||||
|
class bans (base): # pylint: disable=too-few-public-methods, C0103
|
||||||
|
"""
|
||||||
|
Bans table
|
||||||
|
"""
|
||||||
__tablename__ = 'bans'
|
__tablename__ = 'bans'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
ip = Column(String, nullable=False)
|
ip_address = Column(String, nullable=False)
|
||||||
code = Column(Integer, nullable=False)
|
code = Column(Integer, nullable=False)
|
||||||
msg = Column(String, nullable=False)
|
msg = Column(String, nullable=False)
|
||||||
created_at = Column(DateTime, nullable=False)
|
created_at = Column(DateTime, nullable=False)
|
||||||
|
|
||||||
def __init__(self, ip, code, msg):
|
def __init__(self, ip_address, code, msg):
|
||||||
self.ip = ip
|
self.ip_address = ip_address
|
||||||
self.code = code
|
self.code = code
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.created_at = datetime.now()
|
self.created_at = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
base.metadata.create_all(engine)
|
base.metadata.create_all(engine)
|
||||||
|
|
|
@ -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
|
|
|
@ -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) + '%'
|
|
117
gallery/metadata/__init__.py
Normal file
117
gallery/metadata/__init__.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
"""
|
||||||
|
OnlyLegs - Metatada Parser
|
||||||
|
Parse metadata from images if available
|
||||||
|
otherwise get some basic information from the file
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.ExifTags import TAGS, GPSTAGS
|
||||||
|
|
||||||
|
from .helpers import *
|
||||||
|
from .mapping import *
|
||||||
|
|
||||||
|
class Metadata:
|
||||||
|
"""
|
||||||
|
Metadata parser
|
||||||
|
"""
|
||||||
|
def __init__(self, file_path):
|
||||||
|
"""
|
||||||
|
Initialize the metadata parser
|
||||||
|
"""
|
||||||
|
self.file_path = file_path
|
||||||
|
img_exif = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
file = Image.open(file_path)
|
||||||
|
tags = file._getexif()
|
||||||
|
img_exif = {}
|
||||||
|
|
||||||
|
for tag, value in TAGS.items():
|
||||||
|
if tag in tags:
|
||||||
|
img_exif[value] = tags[tag]
|
||||||
|
|
||||||
|
img_exif['FileName'] = os.path.basename(file_path)
|
||||||
|
img_exif['FileSize'] = os.path.getsize(file_path)
|
||||||
|
img_exif['FileFormat'] = img_exif['FileName'].split('.')[-1]
|
||||||
|
img_exif['FileWidth'], img_exif['FileHeight'] = file.size
|
||||||
|
|
||||||
|
file.close()
|
||||||
|
except TypeError:
|
||||||
|
img_exif['FileName'] = os.path.basename(file_path)
|
||||||
|
img_exif['FileSize'] = os.path.getsize(file_path)
|
||||||
|
img_exif['FileFormat'] = img_exif['FileName'].split('.')[-1]
|
||||||
|
img_exif['FileWidth'], img_exif['FileHeight'] = file.size
|
||||||
|
|
||||||
|
self.encoded = img_exif
|
||||||
|
|
||||||
|
def yoink(self):
|
||||||
|
"""
|
||||||
|
Yoinks the metadata from the image
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(self.file_path):
|
||||||
|
return None
|
||||||
|
return self.format_data(self.encoded)
|
||||||
|
|
||||||
|
def format_data(self, encoded_exif): # pylint: disable=R0912 # For now, this is fine
|
||||||
|
"""
|
||||||
|
Formats the data into a dictionary
|
||||||
|
"""
|
||||||
|
exif = {
|
||||||
|
'Photographer': {},
|
||||||
|
'Camera': {},
|
||||||
|
'Software': {},
|
||||||
|
'File': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for data in encoded_exif:
|
||||||
|
if data in PHOTOGRAHER_MAPPING:
|
||||||
|
exif['Photographer'][PHOTOGRAHER_MAPPING[data][0]] = {
|
||||||
|
'raw': encoded_exif[data],
|
||||||
|
}
|
||||||
|
elif data in CAMERA_MAPPING:
|
||||||
|
if len(CAMERA_MAPPING[data]) == 2:
|
||||||
|
exif['Camera'][CAMERA_MAPPING[data][0]] = {
|
||||||
|
'raw': encoded_exif[data],
|
||||||
|
'formatted':
|
||||||
|
getattr(helpers, CAMERA_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
exif['Camera'][CAMERA_MAPPING[data][0]] = {
|
||||||
|
'raw': encoded_exif[data],
|
||||||
|
}
|
||||||
|
elif data in SOFTWARE_MAPPING:
|
||||||
|
if len(SOFTWARE_MAPPING[data]) == 2:
|
||||||
|
exif['Software'][SOFTWARE_MAPPING[data][0]] = {
|
||||||
|
'raw': encoded_exif[data],
|
||||||
|
'formatted':
|
||||||
|
getattr(helpers, SOFTWARE_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
exif['Software'][SOFTWARE_MAPPING[data][0]] = {
|
||||||
|
'raw': encoded_exif[data],
|
||||||
|
}
|
||||||
|
elif data in FILE_MAPPING:
|
||||||
|
if len(FILE_MAPPING[data]) == 2:
|
||||||
|
exif['File'][FILE_MAPPING[data][0]] = {
|
||||||
|
'raw': encoded_exif[data],
|
||||||
|
'formatted':
|
||||||
|
getattr(helpers, FILE_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
exif['File'][FILE_MAPPING[data][0]] = {
|
||||||
|
'raw': encoded_exif[data]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove empty keys
|
||||||
|
if len(exif['Photographer']) == 0:
|
||||||
|
del exif['Photographer']
|
||||||
|
if len(exif['Camera']) == 0:
|
||||||
|
del exif['Camera']
|
||||||
|
if len(exif['Software']) == 0:
|
||||||
|
del exif['Software']
|
||||||
|
if len(exif['File']) == 0:
|
||||||
|
del exif['File']
|
||||||
|
|
||||||
|
return exif
|
407
gallery/metadata/helpers.py
Normal file
407
gallery/metadata/helpers.py
Normal file
|
@ -0,0 +1,407 @@
|
||||||
|
"""
|
||||||
|
OnlyLegs - Metadata Parser
|
||||||
|
Metadata formatting helpers
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def human_size(value):
|
||||||
|
"""
|
||||||
|
Formats the size of a file in a human readable format
|
||||||
|
"""
|
||||||
|
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||||
|
if abs(value) < 1024.0:
|
||||||
|
return f"{value:3.1f}{unit}B"
|
||||||
|
value /= 1024.0
|
||||||
|
|
||||||
|
return f"{value:.1f}YiB"
|
||||||
|
|
||||||
|
|
||||||
|
def date_format(value):
|
||||||
|
"""
|
||||||
|
Formats the date into a standard format
|
||||||
|
"""
|
||||||
|
return str(datetime.strptime(value, '%Y:%m:%d %H:%M:%S'))
|
||||||
|
|
||||||
|
|
||||||
|
def fnumber(value):
|
||||||
|
"""
|
||||||
|
Formats the f-number into a standard format
|
||||||
|
"""
|
||||||
|
return 'f/' + str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def iso(value):
|
||||||
|
"""
|
||||||
|
Formats the ISO into a standard format
|
||||||
|
"""
|
||||||
|
return 'ISO ' + str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def shutter(value):
|
||||||
|
"""
|
||||||
|
Formats the shutter speed into a standard format
|
||||||
|
"""
|
||||||
|
return str(value) + 's'
|
||||||
|
|
||||||
|
|
||||||
|
def focal_length(value):
|
||||||
|
"""
|
||||||
|
Formats the focal length into a standard format
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return str(value[0] / value[1]) + 'mm'
|
||||||
|
except TypeError:
|
||||||
|
return str(value) + 'mm'
|
||||||
|
|
||||||
|
|
||||||
|
def exposure(value):
|
||||||
|
"""
|
||||||
|
Formats the exposure value into a standard format
|
||||||
|
"""
|
||||||
|
return str(value) + 'EV'
|
||||||
|
|
||||||
|
|
||||||
|
def color_space(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the color space to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Reserved',
|
||||||
|
1: 'sRGB',
|
||||||
|
65535: 'Uncalibrated'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def flash(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the flash to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Flash did not fire',
|
||||||
|
1: 'Flash fired',
|
||||||
|
5: 'Strobe return light not detected',
|
||||||
|
7: 'Strobe return light detected',
|
||||||
|
9: 'Flash fired, compulsory flash mode',
|
||||||
|
13: 'Flash fired, compulsory flash mode, return light not detected',
|
||||||
|
15: 'Flash fired, compulsory flash mode, return light detected',
|
||||||
|
16: 'Flash did not fire, compulsory flash mode',
|
||||||
|
24: 'Flash did not fire, auto mode',
|
||||||
|
25: 'Flash fired, auto mode',
|
||||||
|
29: 'Flash fired, auto mode, return light not detected',
|
||||||
|
31: 'Flash fired, auto mode, return light detected',
|
||||||
|
32: 'No flash function',
|
||||||
|
65: 'Flash fired, red-eye reduction mode',
|
||||||
|
69: 'Flash fired, red-eye reduction mode, return light not detected',
|
||||||
|
71: 'Flash fired, red-eye reduction mode, return light detected',
|
||||||
|
73: 'Flash fired, compulsory flash mode, red-eye reduction mode',
|
||||||
|
77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
|
||||||
|
79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
|
||||||
|
89: 'Flash fired, auto mode, red-eye reduction mode',
|
||||||
|
93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
|
||||||
|
95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def exposure_program(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the exposure program to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Not defined',
|
||||||
|
1: 'Manual',
|
||||||
|
2: 'Normal program',
|
||||||
|
3: 'Aperture priority',
|
||||||
|
4: 'Shutter priority',
|
||||||
|
5: 'Creative program',
|
||||||
|
6: 'Action program',
|
||||||
|
7: 'Portrait mode',
|
||||||
|
8: 'Landscape mode'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def metering_mode(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the metering mode to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Unknown',
|
||||||
|
1: 'Average',
|
||||||
|
2: 'Center-Weighted Average',
|
||||||
|
3: 'Spot',
|
||||||
|
4: 'Multi-Spot',
|
||||||
|
5: 'Pattern',
|
||||||
|
6: 'Partial',
|
||||||
|
255: 'Other'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolution_unit(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the resolution unit to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
1: 'No absolute unit of measurement',
|
||||||
|
2: 'Inch',
|
||||||
|
3: 'Centimeter'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def light_source(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the light source to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Unknown',
|
||||||
|
1: 'Daylight',
|
||||||
|
2: 'Fluorescent',
|
||||||
|
3: 'Tungsten (incandescent light)',
|
||||||
|
4: 'Flash',
|
||||||
|
9: 'Fine weather',
|
||||||
|
10: 'Cloudy weather',
|
||||||
|
11: 'Shade',
|
||||||
|
12: 'Daylight fluorescent (D 5700 - 7100K)',
|
||||||
|
13: 'Day white fluorescent (N 4600 - 5400K)',
|
||||||
|
14: 'Cool white fluorescent (W 3900 - 4500K)',
|
||||||
|
15: 'White fluorescent (WW 3200 - 3700K)',
|
||||||
|
17: 'Standard light A',
|
||||||
|
18: 'Standard light B',
|
||||||
|
19: 'Standard light C',
|
||||||
|
20: 'D55',
|
||||||
|
21: 'D65',
|
||||||
|
22: 'D75',
|
||||||
|
23: 'D50',
|
||||||
|
24: 'ISO studio tungsten',
|
||||||
|
255: 'Other light source',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def scene_capture_type(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the scene capture type to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Standard',
|
||||||
|
1: 'Landscape',
|
||||||
|
2: 'Portrait',
|
||||||
|
3: 'Night scene',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def scene_type(value): # pylint: disable=W0613 # Itss fiiiineeee
|
||||||
|
"""
|
||||||
|
Maps the value of the scene type to a human readable format
|
||||||
|
"""
|
||||||
|
return 'Directly photographed image'
|
||||||
|
|
||||||
|
|
||||||
|
def white_balance(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the white balance to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Auto white balance',
|
||||||
|
1: 'Manual white balance',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def exposure_mode(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the exposure mode to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: 'Auto exposure',
|
||||||
|
1: 'Manual exposure',
|
||||||
|
2: 'Auto bracket',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def sensitivity_type(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the sensitivity type to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0:
|
||||||
|
'Unknown',
|
||||||
|
1:
|
||||||
|
'Standard Output Sensitivity',
|
||||||
|
2:
|
||||||
|
'Recommended Exposure Index',
|
||||||
|
3:
|
||||||
|
'ISO Speed',
|
||||||
|
4:
|
||||||
|
'Standard Output Sensitivity and Recommended Exposure Index',
|
||||||
|
5:
|
||||||
|
'Standard Output Sensitivity and ISO Speed',
|
||||||
|
6:
|
||||||
|
'Recommended Exposure Index and ISO Speed',
|
||||||
|
7:
|
||||||
|
'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def lens_specification(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the lens specification to a human readable format
|
||||||
|
"""
|
||||||
|
return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm'
|
||||||
|
|
||||||
|
|
||||||
|
def compression_type(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the compression type to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
1: 'Uncompressed',
|
||||||
|
2: 'CCITT 1D',
|
||||||
|
3: 'T4/Group 3 Fax',
|
||||||
|
4: 'T6/Group 4 Fax',
|
||||||
|
5: 'LZW',
|
||||||
|
6: 'JPEG (old-style)',
|
||||||
|
7: 'JPEG',
|
||||||
|
8: 'Adobe Deflate',
|
||||||
|
9: 'JBIG B&W',
|
||||||
|
10: 'JBIG Color',
|
||||||
|
99: 'JPEG',
|
||||||
|
262: 'Kodak 262',
|
||||||
|
32766: 'Next',
|
||||||
|
32767: 'Sony ARW Compressed',
|
||||||
|
32769: 'Packed RAW',
|
||||||
|
32770: 'Samsung SRW Compressed',
|
||||||
|
32771: 'CCIRLEW',
|
||||||
|
32772: 'Samsung SRW Compressed 2',
|
||||||
|
32773: 'PackBits',
|
||||||
|
32809: 'Thunderscan',
|
||||||
|
32867: 'Kodak KDC Compressed',
|
||||||
|
32895: 'IT8CTPAD',
|
||||||
|
32896: 'IT8LW',
|
||||||
|
32897: 'IT8MP',
|
||||||
|
32898: 'IT8BL',
|
||||||
|
32908: 'PixarFilm',
|
||||||
|
32909: 'PixarLog',
|
||||||
|
32946: 'Deflate',
|
||||||
|
32947: 'DCS',
|
||||||
|
33003: 'Aperio JPEG 2000 YCbCr',
|
||||||
|
33005: 'Aperio JPEG 2000 RGB',
|
||||||
|
34661: 'JBIG',
|
||||||
|
34676: 'SGILog',
|
||||||
|
34677: 'SGILog24',
|
||||||
|
34712: 'JPEG 2000',
|
||||||
|
34713: 'Nikon NEF Compressed',
|
||||||
|
34715: 'JBIG2 TIFF FX',
|
||||||
|
34718: '(MDI) Binary Level Codec',
|
||||||
|
34719: '(MDI) Progressive Transform Codec',
|
||||||
|
34720: '(MDI) Vector',
|
||||||
|
34887: 'ESRI Lerc',
|
||||||
|
34892: 'Lossy JPEG',
|
||||||
|
34925: 'LZMA2',
|
||||||
|
34926: 'Zstd',
|
||||||
|
34927: 'WebP',
|
||||||
|
34933: 'PNG',
|
||||||
|
34934: 'JPEG XR',
|
||||||
|
65000: 'Kodak DCR Compressed',
|
||||||
|
65535: 'Pentax PEF Compressed',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def orientation(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the orientation to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
1: 'Horizontal (normal)',
|
||||||
|
2: 'Mirror horizontal',
|
||||||
|
3: 'Rotate 180',
|
||||||
|
4: 'Mirror vertical',
|
||||||
|
5: 'Mirror horizontal and rotate 270 CW',
|
||||||
|
6: 'Rotate 90 CW',
|
||||||
|
7: 'Mirror horizontal and rotate 90 CW',
|
||||||
|
8: 'Rotate 270 CW',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return value_map[int(value)]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def components_configuration(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the components configuration to a human readable format
|
||||||
|
"""
|
||||||
|
value_map = {
|
||||||
|
0: '',
|
||||||
|
1: 'Y',
|
||||||
|
2: 'Cb',
|
||||||
|
3: 'Cr',
|
||||||
|
4: 'R',
|
||||||
|
5: 'G',
|
||||||
|
6: 'B',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return ''.join([value_map[int(x)] for x in value])
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rating(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the rating to a human readable format
|
||||||
|
"""
|
||||||
|
return str(value) + ' stars'
|
||||||
|
|
||||||
|
|
||||||
|
def rating_percent(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the rating to a human readable format
|
||||||
|
"""
|
||||||
|
return str(value) + '%'
|
||||||
|
|
||||||
|
|
||||||
|
def pixel_dimension(value):
|
||||||
|
"""
|
||||||
|
Maps the value of the pixel dimension to a human readable format
|
||||||
|
"""
|
||||||
|
return str(value) + 'px'
|
62
gallery/metadata/mapping.py
Normal file
62
gallery/metadata/mapping.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
"""
|
||||||
|
OnlyLegs - Metatada Parser
|
||||||
|
Mapping for metadata
|
||||||
|
"""
|
||||||
|
PHOTOGRAHER_MAPPING = {
|
||||||
|
'Artist': ['Artist'],
|
||||||
|
'UserComment': ['Comment'],
|
||||||
|
'ImageDescription': ['Description'],
|
||||||
|
'Copyright': ['Copyright'],
|
||||||
|
}
|
||||||
|
CAMERA_MAPPING = {
|
||||||
|
'Model': ['Model'],
|
||||||
|
'Make': ['Make'],
|
||||||
|
'BodySerialNumber': ['Camera Type'],
|
||||||
|
'LensMake': ['Lens Make'],
|
||||||
|
'LenseModel': ['Lens Model'],
|
||||||
|
'LensSpecification': ['Lens Specification', 'lens_specification'],
|
||||||
|
'ComponentsConfiguration': ['Components Configuration', 'components_configuration'],
|
||||||
|
'DateTime': ['Date Processed', 'date_format'],
|
||||||
|
'DateTimeDigitized': ['Time Digitized', 'date_format'],
|
||||||
|
'OffsetTime': ['Time Offset'],
|
||||||
|
'OffsetTimeOriginal': ['Time Offset - Original'],
|
||||||
|
'OffsetTimeDigitized': ['Time Offset - Digitized'],
|
||||||
|
'DateTimeOriginal': ['Date Original', 'date_format'],
|
||||||
|
'FNumber': ['F-Stop', 'fnumber'],
|
||||||
|
'FocalLength': ['Focal Length', 'focal_length'],
|
||||||
|
'FocalLengthIn35mmFilm': ['Focal Length (35mm format)', 'focal_length'],
|
||||||
|
'MaxApertureValue': ['Max Aperture', 'fnumber'],
|
||||||
|
'ApertureValue': ['Aperture', 'fnumber'],
|
||||||
|
'ShutterSpeedValue': ['Shutter Speed', 'shutter'],
|
||||||
|
'ISOSpeedRatings': ['ISO Speed Ratings', 'iso'],
|
||||||
|
'ISOSpeed': ['ISO Speed', 'iso'],
|
||||||
|
'SensitivityType': ['Sensitivity Type', 'sensitivity_type'],
|
||||||
|
'ExposureBiasValue': ['Exposure Bias', 'ev'],
|
||||||
|
'ExposureTime': ['Exposure Time', 'shutter'],
|
||||||
|
'ExposureMode': ['Exposure Mode', 'exposure_mode'],
|
||||||
|
'ExposureProgram': ['Exposure Program', 'exposure_program'],
|
||||||
|
'WhiteBalance': ['White Balance', 'white_balance'],
|
||||||
|
'Flash': ['Flash', 'flash'],
|
||||||
|
'MeteringMode': ['Metering Mode', 'metering_mode'],
|
||||||
|
'LightSource': ['Light Source', 'light_source'],
|
||||||
|
'SceneCaptureType': ['Scene Capture Type', 'scene_capture_type'],
|
||||||
|
'SceneType': ['Scene Type', 'scene_type'],
|
||||||
|
'Rating': ['Rating', 'rating'],
|
||||||
|
'RatingPercent': ['Rating Percent', 'rating_percent'],
|
||||||
|
}
|
||||||
|
SOFTWARE_MAPPING = {
|
||||||
|
'Software': ['Software'],
|
||||||
|
'ColorSpace': ['Colour Space', 'color_space'],
|
||||||
|
'Compression': ['Compression', 'compression_type'],
|
||||||
|
}
|
||||||
|
FILE_MAPPING = {
|
||||||
|
'FileName': ['Name'],
|
||||||
|
'FileSize': ['Size', 'human_size'],
|
||||||
|
'FileFormat': ['Format'],
|
||||||
|
'FileWidth': ['Width', 'pixel_dimension'],
|
||||||
|
'FileHeight': ['Height', 'pixel_dimension'],
|
||||||
|
'Orientation': ['Orientation', 'orientation'],
|
||||||
|
'XResolution': ['X-resolution'],
|
||||||
|
'YResolution': ['Y-resolution'],
|
||||||
|
'ResolutionUnit': ['Resolution Units', 'resolution_unit'],
|
||||||
|
}
|
|
@ -1,23 +1,27 @@
|
||||||
from flask import Blueprint, render_template, current_app
|
"""
|
||||||
from werkzeug.exceptions import abort
|
Onlylegs Gallery - Routing
|
||||||
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
|
|
||||||
|
|
||||||
import os
|
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__)
|
blueprint = Blueprint('gallery', __name__)
|
||||||
|
db_session = sessionmaker(bind=db.engine)
|
||||||
|
db_session = db_session()
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/')
|
@blueprint.route('/')
|
||||||
def index():
|
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()
|
images = db_session.query(db.posts).order_by(db.posts.id.desc()).all()
|
||||||
|
|
||||||
return render_template('index.html',
|
return render_template('index.html',
|
||||||
|
@ -26,41 +30,45 @@ def index():
|
||||||
name=current_app.config['WEBSITE']['name'],
|
name=current_app.config['WEBSITE']['name'],
|
||||||
motto=current_app.config['WEBSITE']['motto'])
|
motto=current_app.config['WEBSITE']['motto'])
|
||||||
|
|
||||||
|
@blueprint.route('/image/<int:image_id>')
|
||||||
@blueprint.route('/image/<int:id>')
|
def image(image_id):
|
||||||
def image(id):
|
"""
|
||||||
img = db_session.query(db.posts).filter_by(id=id).first()
|
Image view, shows the image and its metadata
|
||||||
|
"""
|
||||||
|
img = db_session.query(db.posts).filter_by(id=image_id).first()
|
||||||
|
|
||||||
if img is None:
|
if img is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
exif = mt.metadata.yoink(
|
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
|
||||||
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)
|
return render_template('image.html', image=img, exif=exif)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/group')
|
@blueprint.route('/group')
|
||||||
def groups():
|
def groups():
|
||||||
|
"""
|
||||||
|
Group overview, shows all image groups
|
||||||
|
"""
|
||||||
return render_template('group.html', group_id='gwa gwa')
|
return render_template('group.html', group_id='gwa gwa')
|
||||||
|
|
||||||
|
@blueprint.route('/group/<int:group_id>')
|
||||||
@blueprint.route('/group/<int:id>')
|
def group(group_id):
|
||||||
def group(id):
|
"""
|
||||||
return render_template('group.html', group_id=id)
|
Group view, shows all images in a group
|
||||||
|
"""
|
||||||
|
return render_template('group.html', group_id=group_id)
|
||||||
@blueprint.route('/upload')
|
|
||||||
@login_required
|
|
||||||
def upload():
|
|
||||||
return render_template('upload.html')
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/profile')
|
@blueprint.route('/profile')
|
||||||
def profile():
|
def profile():
|
||||||
|
"""
|
||||||
|
Profile overview, shows all profiles on the onlylegs gallery
|
||||||
|
"""
|
||||||
return render_template('profile.html', user_id='gwa gwa')
|
return render_template('profile.html', user_id='gwa gwa')
|
||||||
|
|
||||||
|
@blueprint.route('/profile/<int:user_id>')
|
||||||
@blueprint.route('/profile/<int:id>')
|
def profile_id(user_id):
|
||||||
def profile_id(id):
|
"""
|
||||||
return render_template('profile.html', user_id=id)
|
Shows user ofa given id, displays their uploads and other info
|
||||||
|
"""
|
||||||
|
return render_template('profile.html', user_id=user_id)
|
||||||
|
|
|
@ -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)
|
|
|
@ -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 gallery.auth import login_required
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
blueprint = Blueprint('settings', __name__, url_prefix='/settings')
|
blueprint = Blueprint('settings', __name__, url_prefix='/settings')
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/')
|
@blueprint.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
def general():
|
def general():
|
||||||
|
"""
|
||||||
|
General settings page
|
||||||
|
"""
|
||||||
return render_template('settings/general.html')
|
return render_template('settings/general.html')
|
||||||
|
|
||||||
@blueprint.route('/server')
|
@blueprint.route('/server')
|
||||||
@login_required
|
@login_required
|
||||||
def server():
|
def server():
|
||||||
|
"""
|
||||||
|
Server settings page
|
||||||
|
"""
|
||||||
return render_template('settings/server.html')
|
return render_template('settings/server.html')
|
||||||
|
|
||||||
@blueprint.route('/account')
|
@blueprint.route('/account')
|
||||||
@login_required
|
@login_required
|
||||||
def account():
|
def account():
|
||||||
|
"""
|
||||||
|
Account settings page
|
||||||
|
"""
|
||||||
return render_template('settings/account.html')
|
return render_template('settings/account.html')
|
||||||
|
|
||||||
@blueprint.route('/logs')
|
@blueprint.route('/logs')
|
||||||
@login_required
|
@login_required
|
||||||
def logs():
|
def logs():
|
||||||
return render_template('settings/logs.html')
|
"""
|
||||||
|
Logs settings page
|
||||||
|
"""
|
||||||
|
return render_template('settings/logs.html')
|
||||||
|
|
|
@ -1,49 +1,66 @@
|
||||||
# Import dependencies
|
"""
|
||||||
import platformdirs
|
OnlyLegs - Setup
|
||||||
|
Runs when the app detects that there is no user directory
|
||||||
|
"""
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import platformdirs
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
class setup:
|
USER_DIR = platformdirs.user_config_dir('onlylegs')
|
||||||
def __init__(self):
|
|
||||||
self.user_dir = platformdirs.user_config_dir('onlylegs')
|
|
||||||
|
|
||||||
|
class SetupApp:
|
||||||
|
"""
|
||||||
|
Setup the application on first run
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Main setup function
|
||||||
|
"""
|
||||||
print("Running setup...")
|
print("Running setup...")
|
||||||
|
|
||||||
if not os.path.exists(self.user_dir):
|
if not os.path.exists(USER_DIR):
|
||||||
self.make_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()
|
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()
|
self.make_yaml()
|
||||||
|
|
||||||
def make_dir(self):
|
def make_dir(self):
|
||||||
|
"""
|
||||||
|
Create the user directory
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
os.makedirs(self.user_dir)
|
os.makedirs(USER_DIR)
|
||||||
os.makedirs(os.path.join(self.user_dir, 'instance'))
|
os.makedirs(os.path.join(USER_DIR, 'instance'))
|
||||||
|
|
||||||
print("Created user directory at:", self.user_dir)
|
print("Created user directory at:", USER_DIR)
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
print("Error creating user directory:", e)
|
print("Error creating user directory:", err)
|
||||||
exit(1) # exit with error code
|
sys.exit(1) # exit with error code
|
||||||
|
|
||||||
def make_env(self):
|
def make_env(self):
|
||||||
# Create .env file with default values
|
"""
|
||||||
|
Create the .env file with default values
|
||||||
|
"""
|
||||||
env_conf = {
|
env_conf = {
|
||||||
'FLASK_SECRETE': 'dev',
|
'FLASK_SECRETE': 'dev',
|
||||||
}
|
}
|
||||||
try:
|
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():
|
for key, value in env_conf.items():
|
||||||
f.write(f"{key}={value}\n")
|
file.write(f"{key}={value}\n")
|
||||||
print("Created environment variables")
|
print("Created environment variables")
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
print("Error creating environment variables:", e)
|
print("Error creating environment variables:", err)
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("Generated default .env file. EDIT IT BEFORE RUNNING THE APP AGAIN!")
|
print("Generated default .env file. EDIT IT BEFORE RUNNING THE APP AGAIN!")
|
||||||
|
|
||||||
def make_yaml(self):
|
def make_yaml(self):
|
||||||
# Create yaml config file with default values
|
"""
|
||||||
|
Create the YAML config file with default values
|
||||||
|
"""
|
||||||
yaml_conf = {
|
yaml_conf = {
|
||||||
'admin': {
|
'admin': {
|
||||||
'name': 'Real Person',
|
'name': 'Real Person',
|
||||||
|
@ -71,11 +88,11 @@ class setup:
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(self.user_dir, 'conf.yml'), 'w') as f:
|
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as file:
|
||||||
yaml.dump(yaml_conf, f, default_flow_style=False)
|
yaml.dump(yaml_conf, file, default_flow_style=False)
|
||||||
print("Created default gallery config")
|
print("Created default gallery config")
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
print("Error creating default gallery config:", e)
|
print("Error creating default gallery config:", err)
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("Generated default YAML config. EDIT IT BEFORE RUNNING THE APP AGAIN!")
|
print("Generated default YAML config. EDIT IT BEFORE RUNNING THE APP AGAIN!")
|
||||||
|
|
78
gallery/theme_manager.py
Normal file
78
gallery/theme_manager.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
"""
|
||||||
|
OnlyLegs - Theme Manager
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
import sass
|
||||||
|
|
||||||
|
|
||||||
|
class CompileTheme():
|
||||||
|
"""
|
||||||
|
Compiles the theme into the static folder
|
||||||
|
"""
|
||||||
|
def __init__(self, theme_name, app_path):
|
||||||
|
"""
|
||||||
|
Initialize the theme manager
|
||||||
|
Compiles the theme into the static folder and loads the fonts
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(f"Loading '{theme_name}' theme...")
|
||||||
|
|
||||||
|
theme_path = os.path.join(app_path, 'themes', theme_name)
|
||||||
|
theme_dest = os.path.join(app_path, 'static', 'theme')
|
||||||
|
|
||||||
|
if not os.path.exists(theme_path):
|
||||||
|
print("Theme does not exist!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.load_sass(theme_path, theme_dest)
|
||||||
|
self.load_fonts(theme_path, theme_dest)
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
print(f"{now.hour}:{now.minute}:{now.second} - Done!\n")
|
||||||
|
|
||||||
|
def load_sass(self, source_path, css_dest):
|
||||||
|
"""
|
||||||
|
Compile the sass (or scss) file into css and save it to the static folder
|
||||||
|
"""
|
||||||
|
if os.path.join(source_path, 'style.sass'):
|
||||||
|
sass_path = os.path.join(source_path, 'style.sass')
|
||||||
|
elif os.path.join(source_path, 'style.scss'):
|
||||||
|
sass_path = os.path.join(source_path, 'style.scss')
|
||||||
|
else:
|
||||||
|
print("No sass file found!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(os.path.join(css_dest, 'style.css'), 'w', encoding='utf-8') as file:
|
||||||
|
try:
|
||||||
|
file.write(sass.compile(filename=sass_path,output_style='compressed'))
|
||||||
|
except sass.CompileError as err:
|
||||||
|
print("Failed to compile!\n", err)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Compiled successfully!")
|
||||||
|
|
||||||
|
def load_fonts(self, source_path, font_dest):
|
||||||
|
"""
|
||||||
|
Copy the fonts folder to the static folder
|
||||||
|
"""
|
||||||
|
# Append fonts to the destination path
|
||||||
|
source_path = os.path.join(source_path, 'fonts')
|
||||||
|
font_dest = os.path.join(font_dest, 'fonts')
|
||||||
|
|
||||||
|
if os.path.exists(font_dest):
|
||||||
|
print("Updating fonts...")
|
||||||
|
try:
|
||||||
|
shutil.rmtree(font_dest)
|
||||||
|
except Exception as err:
|
||||||
|
print("Failed to remove old fonts!\n", err)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.copytree(source_path, font_dest)
|
||||||
|
print("Copied new fonts!")
|
||||||
|
except Exception as err:
|
||||||
|
print("Failed to copy fonts!\n", err)
|
||||||
|
sys.exit(1)
|
297
poetry.lock
generated
297
poetry.lock
generated
|
@ -1,5 +1,25 @@
|
||||||
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
|
# 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]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
|
@ -134,6 +154,21 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
Pillow = "*"
|
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]]
|
[[package]]
|
||||||
name = "flask"
|
name = "flask"
|
||||||
version = "2.2.3"
|
version = "2.2.3"
|
||||||
|
@ -148,6 +183,7 @@ files = [
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
click = ">=8.0"
|
click = ">=8.0"
|
||||||
|
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
|
||||||
itsdangerous = ">=2.0"
|
itsdangerous = ">=2.0"
|
||||||
Jinja2 = ">=3.0"
|
Jinja2 = ">=3.0"
|
||||||
Werkzeug = ">=2.2.2"
|
Werkzeug = ">=2.2.2"
|
||||||
|
@ -267,6 +303,44 @@ gevent = ["gevent (>=1.4.0)"]
|
||||||
setproctitle = ["setproctitle"]
|
setproctitle = ["setproctitle"]
|
||||||
tornado = ["tornado (>=0.2)"]
|
tornado = ["tornado (>=0.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "6.0.0"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"},
|
||||||
|
{file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||||
|
perf = ["ipython"]
|
||||||
|
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "5.12.0"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8.0"
|
||||||
|
files = [
|
||||||
|
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
|
||||||
|
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colors = ["colorama (>=0.4.3)"]
|
||||||
|
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
||||||
|
plugins = ["setuptools"]
|
||||||
|
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itsdangerous"
|
name = "itsdangerous"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
|
@ -297,6 +371,52 @@ MarkupSafe = ">=2.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
i18n = ["Babel (>=2.7)"]
|
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]]
|
[[package]]
|
||||||
name = "libsass"
|
name = "libsass"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
|
@ -372,6 +492,18 @@ files = [
|
||||||
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
{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]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "9.4.0"
|
version = "9.4.0"
|
||||||
|
@ -465,20 +597,50 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
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\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
|
{file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
|
||||||
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
|
{file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
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)"]
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pylint"
|
||||||
|
version = "2.16.3"
|
||||||
|
description = "python code static checker"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7.2"
|
||||||
|
files = [
|
||||||
|
{file = "pylint-2.16.3-py3-none-any.whl", hash = "sha256:3e803be66e3a34c76b0aa1a3cf4714b538335e79bd69718d34fcf36d8fff2a2b"},
|
||||||
|
{file = "pylint-2.16.3.tar.gz", hash = "sha256:0decdf8dfe30298cd9f8d82e9a1542da464db47da60e03641631086671a03621"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
astroid = ">=2.14.2,<=2.16.0-dev0"
|
||||||
|
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||||
|
dill = [
|
||||||
|
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
||||||
|
{version = ">=0.3.6", markers = "python_version >= \"3.11\""},
|
||||||
|
]
|
||||||
|
isort = ">=4.2.5,<6"
|
||||||
|
mccabe = ">=0.6,<0.8"
|
||||||
|
platformdirs = ">=2.2.0"
|
||||||
|
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||||
|
tomlkit = ">=0.10.1"
|
||||||
|
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
spelling = ["pyenchant (>=3.2,<4.0)"]
|
||||||
|
testutils = ["gitpython (>3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
|
@ -639,6 +801,30 @@ postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||||
pymysql = ["pymysql"]
|
pymysql = ["pymysql"]
|
||||||
sqlcipher = ["sqlcipher3-binary"]
|
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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.5.0"
|
version = "4.5.0"
|
||||||
|
@ -669,7 +855,108 @@ MarkupSafe = ">=2.1.1"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
watchdog = ["watchdog"]
|
watchdog = ["watchdog"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wrapt"
|
||||||
|
version = "1.15.0"
|
||||||
|
description = "Module for decorators, wrappers and monkey patching."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
|
||||||
|
{file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
|
||||||
|
{file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
|
||||||
|
{file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
|
||||||
|
{file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
|
||||||
|
{file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
|
||||||
|
{file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
|
||||||
|
{file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
|
||||||
|
{file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
|
||||||
|
{file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
|
||||||
|
{file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
|
||||||
|
{file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
|
||||||
|
{file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
|
||||||
|
{file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
|
||||||
|
{file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
|
||||||
|
{file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.15.0"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
|
||||||
|
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||||
|
testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.9"
|
||||||
content-hash = "8577c3b41be81184b268f983c0958e58169f3df0c179b296f3d4be40e0865737"
|
content-hash = "de131da70fd04213714611f747ff9102979dbc6855e68645ea93fa83a6d433d8"
|
||||||
|
|
|
@ -6,9 +6,8 @@ authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = ".github/README.md"
|
readme = ".github/README.md"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.8"
|
||||||
Flask = "^2.2.2"
|
Flask = "^2.2.2"
|
||||||
flask-compress = "^1.13"
|
flask-compress = "^1.13"
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
|
@ -19,8 +18,14 @@ colorthief = "^0.2.1"
|
||||||
Pillow = "^9.4.0"
|
Pillow = "^9.4.0"
|
||||||
platformdirs = "^3.0.0"
|
platformdirs = "^3.0.0"
|
||||||
SQLAlchemy = "^2.0.3"
|
SQLAlchemy = "^2.0.3"
|
||||||
|
pylint = "^2.16.3"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue