Fix for flask limiter 4.0
Fix reseting flask_limiter keys
This commit is contained in:
82
cps/kobo.py
82
cps/kobo.py
@@ -19,38 +19,30 @@
|
||||
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
from functools import wraps
|
||||
import os
|
||||
import uuid
|
||||
import zipfile
|
||||
from time import gmtime, strftime
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
import requests
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
request,
|
||||
make_response,
|
||||
jsonify,
|
||||
current_app,
|
||||
url_for,
|
||||
redirect,
|
||||
abort
|
||||
)
|
||||
from .cw_login import current_user
|
||||
from flask import Blueprint, request, make_response, jsonify, current_app, url_for, redirect, abort, g
|
||||
from werkzeug.datastructures import Headers
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.sql.expression import and_, or_
|
||||
from sqlalchemy.exc import StatementError
|
||||
import requests
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
from .cw_login import current_user, login_user
|
||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||
from . import isoLanguages
|
||||
from .epub import get_epub_layout
|
||||
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE, BASE_DIR
|
||||
from .helper import get_download_link
|
||||
from .services import SyncToken as SyncToken
|
||||
from .web import download_required
|
||||
from .kobo_auth import requires_kobo_auth, get_auth_token
|
||||
from . import isoLanguages, limiter
|
||||
|
||||
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
||||
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
||||
@@ -65,6 +57,33 @@ kobo_auth.register_url_value_preprocessor(kobo)
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def get_auth_token():
|
||||
if "auth_token" in g:
|
||||
return g.get("auth_token")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def requires_kobo_auth(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
auth_token = get_auth_token()
|
||||
if auth_token is not None:
|
||||
user = (
|
||||
ub.session.query(ub.User)
|
||||
.join(ub.RemoteAuthToken)
|
||||
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
||||
.first()
|
||||
)
|
||||
if user is not None:
|
||||
login_user(user)
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return f(*args, **kwargs)
|
||||
log.debug("Received Kobo request without a recognizable auth token.")
|
||||
return abort(401)
|
||||
return inner
|
||||
|
||||
|
||||
def get_store_url_for_current_request():
|
||||
# Programmatically modify the current url to point to the official Kobo store
|
||||
__, __, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
|
||||
@@ -140,7 +159,7 @@ def convert_to_kobo_timestamp_string(timestamp):
|
||||
|
||||
@kobo.route("/v1/library/sync")
|
||||
@requires_kobo_auth
|
||||
# @download_required
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleSyncRequest():
|
||||
if not current_user.role_download():
|
||||
log.info("Users need download permissions for syncing library to Kobo reader")
|
||||
@@ -336,6 +355,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
@kobo.route("/v1/library/<book_uuid>/metadata")
|
||||
@requires_kobo_auth
|
||||
@download_required
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleMetadataRequest(book_uuid):
|
||||
if not current_app.wsgi_app.is_proxied:
|
||||
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
||||
@@ -368,7 +388,7 @@ def get_download_url_for_book(book_id, book_format):
|
||||
)
|
||||
return url_for(
|
||||
"kobo.download_book",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_id=book_id,
|
||||
book_format=book_format.lower(),
|
||||
_external=True,
|
||||
@@ -503,9 +523,11 @@ def get_metadata(book):
|
||||
return metadata
|
||||
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||
def HandleTagCreate():
|
||||
# catch delete requests, otherwise they are handled in the book delete handler
|
||||
@@ -541,6 +563,7 @@ def HandleTagCreate():
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleTagUpdate(tag_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id,
|
||||
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||
@@ -595,6 +618,7 @@ def add_items_to_shelf(items, shelf):
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleTagAddItem(tag_id):
|
||||
items = None
|
||||
try:
|
||||
@@ -625,6 +649,7 @@ def HandleTagAddItem(tag_id):
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleTagRemoveItem(tag_id):
|
||||
items = None
|
||||
try:
|
||||
@@ -758,6 +783,7 @@ def create_kobo_tag(shelf):
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleStateRequest(book_uuid):
|
||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||
if not book or not book.data:
|
||||
@@ -907,6 +933,7 @@ def get_current_bookmark_response(current_bookmark):
|
||||
@kobo.route("/<book_uuid>/<width>/<height>/<isGreyscale>/image.jpg", defaults={'Quality': ""})
|
||||
@kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg")
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
||||
try:
|
||||
if int(height) > 1000:
|
||||
@@ -943,6 +970,7 @@ def TopLevelEndpoint():
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleBookDeletionRequest(book_uuid):
|
||||
log.info("Kobo book delete request received for book %s" % book_uuid)
|
||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||
@@ -962,6 +990,7 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
|
||||
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
|
||||
def HandleUnimplementedRequest(dummy=None):
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
|
||||
request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
@@ -976,6 +1005,9 @@ def HandleUnimplementedRequest(dummy=None):
|
||||
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/assets", methods=["GET"])
|
||||
def HandleUserRequest(dummy=None):
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
log.error("Key: {}".format(limiter.current_limit.key))
|
||||
log.error("Remaining: {}".format(limiter.current_limit.remaining))
|
||||
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
@@ -983,6 +1015,7 @@ def HandleUserRequest(dummy=None):
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
||||
def handle_benefits():
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
@@ -992,6 +1025,7 @@ def handle_benefits():
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
||||
def handle_getests():
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
@@ -1018,6 +1052,7 @@ def handle_getests():
|
||||
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
||||
def HandleProductsRequest(dummy=None):
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)",
|
||||
request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
@@ -1046,7 +1081,10 @@ def make_calibre_web_auth_response():
|
||||
@kobo.route("/v1/auth/refresh", methods=["POST"])
|
||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleAuthRequest():
|
||||
log.error(limiter.current_limit)
|
||||
log.error(limiter.current_limit)
|
||||
log.debug('Kobo Auth request')
|
||||
if config.config_kobo_proxy:
|
||||
try:
|
||||
@@ -1058,6 +1096,7 @@ def HandleAuthRequest():
|
||||
|
||||
@kobo.route("/v1/initialization")
|
||||
@requires_kobo_auth
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def HandleInitRequest():
|
||||
log.info('Init')
|
||||
|
||||
@@ -1088,7 +1127,7 @@ def HandleInitRequest():
|
||||
kobo_resources["image_host"] = calibre_web_url
|
||||
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
|
||||
url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
@@ -1096,7 +1135,7 @@ def HandleInitRequest():
|
||||
isGreyscale='isGreyscale'))
|
||||
kobo_resources["image_url_template"] = unquote(calibre_web_url +
|
||||
url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
@@ -1104,7 +1143,7 @@ def HandleInitRequest():
|
||||
else:
|
||||
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
|
||||
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
@@ -1112,7 +1151,7 @@ def HandleInitRequest():
|
||||
isGreyscale='isGreyscale',
|
||||
_external=True))
|
||||
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
@@ -1128,6 +1167,7 @@ def HandleInitRequest():
|
||||
@kobo.route("/download/<book_id>/<book_format>")
|
||||
@requires_kobo_auth
|
||||
@download_required
|
||||
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||
def download_book(book_id, book_format):
|
||||
return get_download_link(book_id, book_format, "kobo")
|
||||
|
||||
@@ -1299,3 +1339,5 @@ def NATIVE_KOBO_RESOURCES():
|
||||
"userguide_host": "https://ereaderfiles.kobo.com",
|
||||
"wishlist_page": "https://www.kobo.com/{region}/{language}/account/wishlist"
|
||||
}
|
||||
|
||||
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||
|
||||
@@ -62,14 +62,12 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from os import urandom
|
||||
from functools import wraps
|
||||
|
||||
from flask import g, Blueprint, abort, request
|
||||
from .cw_login import login_user, current_user
|
||||
from flask import g, Blueprint, request
|
||||
from .cw_login import current_user
|
||||
from flask_babel import gettext as _
|
||||
from flask_limiter import RateLimitExceeded
|
||||
|
||||
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
||||
from . import logger, config, calibre_db, db, helper, ub, lm
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import user_login_required
|
||||
|
||||
@@ -135,13 +133,6 @@ def disable_failed_auth_redirect_for_blueprint(bp):
|
||||
lm.blueprint_login_views[bp.name] = None
|
||||
|
||||
|
||||
def get_auth_token():
|
||||
if "auth_token" in g:
|
||||
return g.get("auth_token")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def register_url_value_preprocessor(kobo):
|
||||
@kobo.url_value_preprocessor
|
||||
# pylint: disable=unused-variable
|
||||
@@ -149,28 +140,3 @@ def register_url_value_preprocessor(kobo):
|
||||
g.auth_token = values.pop("auth_token")
|
||||
|
||||
|
||||
def requires_kobo_auth(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
auth_token = get_auth_token()
|
||||
if auth_token is not None:
|
||||
#try:
|
||||
# limiter.check()
|
||||
#except RateLimitExceeded:
|
||||
# return abort(429)
|
||||
#except (ConnectionError, Exception) as e:
|
||||
# log.error("Connection error to limiter backend: %s", e)
|
||||
# return abort(429)
|
||||
user = (
|
||||
ub.session.query(ub.User)
|
||||
.join(ub.RemoteAuthToken)
|
||||
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
||||
.first()
|
||||
)
|
||||
if user is not None:
|
||||
login_user(user)
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
return f(*args, **kwargs)
|
||||
log.debug("Received Kobo request without a recognizable auth token.")
|
||||
return abort(401)
|
||||
return inner
|
||||
|
||||
@@ -46,11 +46,10 @@ def main():
|
||||
try:
|
||||
from .kobo import kobo, get_kobo_activated
|
||||
from .kobo_auth import kobo_auth
|
||||
from flask_limiter.util import get_remote_address
|
||||
kobo_available = get_kobo_activated()
|
||||
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||
kobo_available = False
|
||||
kobo = kobo_auth = get_remote_address = None
|
||||
kobo = kobo_auth = None
|
||||
|
||||
try:
|
||||
from .oauth_bb import oauth
|
||||
@@ -77,7 +76,6 @@ def main():
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
if kobo_available:
|
||||
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||
app.register_blueprint(kobo)
|
||||
app.register_blueprint(kobo_auth)
|
||||
if oauth_available:
|
||||
|
||||
@@ -43,14 +43,14 @@ def verify_password(username, password):
|
||||
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
||||
login_result, error = services.ldap.bind_user(user.name, password)
|
||||
if login_result:
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return user
|
||||
if error is not None:
|
||||
log.error(error)
|
||||
else:
|
||||
# limiter.check()
|
||||
if check_password_hash(str(user.password), password):
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return user
|
||||
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
||||
@@ -120,7 +120,7 @@ def load_user_from_reverse_proxy_header(req):
|
||||
if rp_header_username:
|
||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
|
||||
if user:
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
24
cps/web.py
24
cps/web.py
@@ -30,7 +30,6 @@ from flask import session as flask_session
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import get_locale
|
||||
from .cw_login import login_user, logout_user, current_user
|
||||
# from flask_limiter import RateLimitExceeded
|
||||
from flask_limiter.util import get_remote_address
|
||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
||||
from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
|
||||
@@ -39,7 +38,7 @@ from sqlalchemy.sql.functions import coalesce
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from . import constants, logger, isoLanguages, services
|
||||
from . import constants, logger, isoLanguages, services, limiter
|
||||
from . import db, ub, config, app
|
||||
from . import calibre_db, kobo_sync_status
|
||||
from .search import render_search_results, render_adv_search_results
|
||||
@@ -55,7 +54,6 @@ from .usermanagement import login_required_if_no_ano
|
||||
from .kobo_sync_status import remove_synced_book
|
||||
from .render_template import render_title_template
|
||||
from .kobo_sync_status import change_archived_books
|
||||
from . import limiter
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks_status import render_task_status
|
||||
from .usermanagement import user_login_required
|
||||
@@ -1285,15 +1283,6 @@ def register_post():
|
||||
if not config.config_public_reg:
|
||||
abort(404)
|
||||
to_save = request.form.to_dict()
|
||||
#try:
|
||||
# limiter.check()
|
||||
#except RateLimitExceeded:
|
||||
# flash(_(u"Please wait one minute to register next user"), category="error")
|
||||
# return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
||||
#except (ConnectionError, Exception) as e:
|
||||
# log.error("Connection error to limiter backend: %s", e)
|
||||
# flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
|
||||
# return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
||||
if current_user is not None and current_user.is_authenticated:
|
||||
return redirect(url_for('web.index'))
|
||||
if not config.get_mail_server_configured():
|
||||
@@ -1358,7 +1347,7 @@ def register():
|
||||
def handle_login_user(user, remember, message, category):
|
||||
login_user(user, remember=remember)
|
||||
flash(message, category=category)
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
||||
|
||||
|
||||
@@ -1392,15 +1381,6 @@ def login():
|
||||
def login_post():
|
||||
form = request.form.to_dict()
|
||||
username = strip_whitespaces(form.get('username', "")).lower().replace("\n","").replace("\r","")
|
||||
#try:
|
||||
# limiter.check()
|
||||
#except RateLimitExceeded:
|
||||
# flash(_("Please wait one minute before next login"), category="error")
|
||||
# return render_login(username, form.get("password", ""))
|
||||
#except (ConnectionError, Exception) as e:
|
||||
# log.error("Connection error to limiter backend: %s", e)
|
||||
# flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
|
||||
# return render_login(username, form.get("password", ""))
|
||||
if current_user is not None and current_user.is_authenticated:
|
||||
return redirect(url_for('web.index'))
|
||||
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
|
||||
|
||||
@@ -33,26 +33,26 @@ dependencies = [
|
||||
"Flask>=1.0.2,<3.2.0",
|
||||
"iso-639>=0.4.5,<0.5.0;python_version<'3.12'",
|
||||
"pycountry>=20.0.0,<25.0.0;python_version>='3.12'",
|
||||
"PyPDF>=6.1.3,<6.5.0",
|
||||
"PyPDF>=6.1.3,<6.7.0",
|
||||
"pytz>=2016.10",
|
||||
"requests>=2.32.0,<2.33.0",
|
||||
"SQLAlchemy>=1.3.0,<2.1.0",
|
||||
"tornado>=6.4.2,<6.6",
|
||||
"Wand>=0.4.4,<0.7.0",
|
||||
"unidecode>=0.04.19,<1.4.0",
|
||||
"unidecode>=0.04.19,<1.5.0",
|
||||
"lxml>=4.9.1,<5.4.0",
|
||||
"flask-wtf>=0.14.2,<1.3.0",
|
||||
"chardet>=3.0.0,<5.3.0",
|
||||
"netifaces-plus>=0.12.0,<0.13.0",
|
||||
"urllib3>=1.22,<3.0",
|
||||
"Flask-Limiter>=2.3.0,<3.13.0",
|
||||
"regex>=2022.3.2,<2025.3.20",
|
||||
"bleach>=6.0.0,<6.3.0",
|
||||
"Flask-Limiter>=2.3.0,<4.2.0",
|
||||
"regex>=2022.3.2,<2026.1.16",
|
||||
"bleach>=6.0.0,<6.4.0",
|
||||
"python-magic>=0.4.27,<0.5.0",
|
||||
"python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'",
|
||||
"flask-httpAuth>=4.4.0,<5.0.0",
|
||||
"cryptography>=39.0.0,<45.0.0",
|
||||
"certifi>=2024.7.4,<2025.8.24",
|
||||
"cryptography>=39.0.0,<47.0.0",
|
||||
"certifi>=2024.7.4,<2026.1.5",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
@@ -70,9 +70,9 @@ content-type = "text/markdown"
|
||||
[project.optional-dependencies]
|
||||
gdrive = [
|
||||
"google-api-python-client>=2.73.00,<2.200.0",
|
||||
"gevent>20.6.0,<24.12.0",
|
||||
"greenlet>=0.4.17,<3.3.0",
|
||||
"httplib2>=0.9.2,<0.23.0",
|
||||
"gevent>20.6.0,<25.9.2",
|
||||
"greenlet>=0.4.17,<3.4.0",
|
||||
"httplib2>=0.9.2,<0.32.0",
|
||||
"oauth2client>=4.0.0,<4.1.4",
|
||||
"uritemplate>=3.0.0,<4.3.0",
|
||||
"pyasn1-modules>=0.0.8,<0.7.0",
|
||||
@@ -91,19 +91,19 @@ goodreads = [
|
||||
]
|
||||
ldap = [
|
||||
"python-ldap>=3.0.0,<3.5.0",
|
||||
"Flask-SimpleLDAP>=1.4.0,<2.1.0",
|
||||
"Flask-SimpleLDAP>=1.4.0,<2.2.0",
|
||||
]
|
||||
oauth = [
|
||||
"Flask-Dance>=2.0.0,<7.2.0",
|
||||
"SQLAlchemy-Utils>=0.33.5,<0.42.0",
|
||||
"SQLAlchemy-Utils>=0.33.5,<0.43.0",
|
||||
]
|
||||
metadata = [
|
||||
"rarfile>=3.2,<5.0",
|
||||
"scholarly>=1.2.0,<1.8",
|
||||
"markdown2>=2.0.0,<2.6.0",
|
||||
"html2text>=2020.1.16,<2025.2.26",
|
||||
"html2text>=2020.1.16,<2025.4.16",
|
||||
"python-dateutil>=2.1,<2.10.0",
|
||||
"beautifulsoup4>=4.0.1,<4.14.0",
|
||||
"beautifulsoup4>=4.0.1,<4.15.0",
|
||||
"faust-cchardet>=2.1.18,<2.1.20",
|
||||
"py7zr>=0.15.0,<0.21.0",
|
||||
"mutagen>=1.40.0,<1.50.0",
|
||||
|
||||
@@ -37,20 +37,20 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
|
||||
|
||||
<p class='text-justify attribute'><strong>Start Time: </strong>2026-01-24 20:50:12</p>
|
||||
<p class='text-justify attribute'><strong>Start Time: </strong>2026-02-17 19:21:22</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-md-6 col-sm-offset-3">
|
||||
|
||||
<p class='text-justify attribute'><strong>Stop Time: </strong>2026-01-25 04:15:44</p>
|
||||
<p class='text-justify attribute'><strong>Stop Time: </strong>2026-02-18 02:50:40</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-md-6 col-sm-offset-3">
|
||||
<p class='text-justify attribute'><strong>Duration: </strong>6h 13 min</p>
|
||||
<p class='text-justify attribute'><strong>Duration: </strong>6h 14 min</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2039,13 +2039,13 @@
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="errorClass">
|
||||
<tr id="su" class="failClass">
|
||||
<td>TestLoadMetadata</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">
|
||||
<a onclick="showClassDetail('c18', 1)">Detail</a>
|
||||
</td>
|
||||
@@ -2053,27 +2053,26 @@
|
||||
|
||||
|
||||
|
||||
<tr id="et18.1" class="none bg-info">
|
||||
<tr id="ft18.1" class="none bg-danger">
|
||||
<td>
|
||||
<div class='testcase'>TestLoadMetadata - test_load_metadata</div>
|
||||
</td>
|
||||
<td colspan='6'>
|
||||
<div class="text-center">
|
||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et18.1')">ERROR</a>
|
||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft18.1')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_et18.1" class="popup_window test_output" style="display:block;">
|
||||
<div id="div_ft18.1" class="popup_window test_output" style="display:block;">
|
||||
<div class='close_button pull-right'>
|
||||
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
|
||||
onclick="document.getElementById('div_et18.1').style.display='none'"><span
|
||||
onclick="document.getElementById('div_ft18.1').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 87, in test_load_metadata
|
||||
if results[cont]['source'] == 'https://comicvine.gamespot.com/':
|
||||
~~~~~~~^^^^^^
|
||||
IndexError: list index out of range</pre>
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 220, in test_load_metadata
|
||||
self.assertLessEqual(diff(BytesIO(cover), BytesIO(new_cover), delete_diff_file=True), 0.025)
|
||||
AssertionError: 0.04218017335689641 not less than or equal to 0.025</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -2270,13 +2269,13 @@ IndexError: list index out of range</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="failClass">
|
||||
<tr id="su" class="passClass">
|
||||
<td>TestLoadMetadataScholar</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">
|
||||
<a onclick="showClassDetail('c20', 1)">Detail</a>
|
||||
</td>
|
||||
@@ -2284,31 +2283,11 @@ IndexError: list index out of range</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="ft20.1" class="none bg-danger">
|
||||
<tr id='pt20.1' class='hiddenRow bg-success'>
|
||||
<td>
|
||||
<div class='testcase'>TestLoadMetadataScholar - test_load_metadata</div>
|
||||
</td>
|
||||
<td colspan='6'>
|
||||
<div class="text-center">
|
||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft20.1')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_ft20.1" class="popup_window test_output" style="display:block;">
|
||||
<div class='close_button pull-right'>
|
||||
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
|
||||
onclick="document.getElementById('div_ft20.1').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_edit_metadata_scholar.py", line 74, in test_load_metadata
|
||||
self.assertEqual(30, len(results))
|
||||
AssertionError: 30 != 20</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
<td colspan='6' align='center'>PASS</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -4684,11 +4663,11 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="failClass">
|
||||
<tr id="su" class="skipClass">
|
||||
<td>TestThumbnails</td>
|
||||
<td class="text-center">8</td>
|
||||
<td class="text-center">6</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">7</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">
|
||||
@@ -4761,31 +4740,11 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="ft52.8" class="none bg-danger">
|
||||
<tr id='pt52.8' class='hiddenRow bg-success'>
|
||||
<td>
|
||||
<div class='testcase'>TestThumbnails - test_sideloaded_book</div>
|
||||
</td>
|
||||
<td colspan='6'>
|
||||
<div class="text-center">
|
||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft52.8')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_ft52.8" class="popup_window test_output" style="display:block;">
|
||||
<div class='close_button pull-right'>
|
||||
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
|
||||
onclick="document.getElementById('div_ft52.8').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_thumbnails.py", line 317, in test_sideloaded_book
|
||||
self.assertAlmostEqual(diff(BytesIO(list_cover), BytesIO(old_list_cover), delete_diff_file=True), 0.0,
|
||||
AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182775 difference)</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
<td colspan='6' align='center'>PASS</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -5992,9 +5951,9 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
<tr id='total_row' class="text-center bg-grey">
|
||||
<td>Total</td>
|
||||
<td>538</td>
|
||||
<td>528</td>
|
||||
<td>2</td>
|
||||
<td>530</td>
|
||||
<td>1</td>
|
||||
<td>0</td>
|
||||
<td>7</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
@@ -6023,7 +5982,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<td>Linux 6.8.0-90-generic #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025 x86_64 x86_64</td>
|
||||
<td>Linux 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64 x86_64</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6041,19 +6000,19 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>babel</th>
|
||||
<td>2.17.0</td>
|
||||
<td>2.18.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>bleach</th>
|
||||
<td>6.2.0</td>
|
||||
<td>6.3.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>certifi</th>
|
||||
<td>2025.8.3</td>
|
||||
<td>2026.1.4</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6065,7 +6024,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>cryptography</th>
|
||||
<td>44.0.3</td>
|
||||
<td>46.0.5</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6089,7 +6048,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>Flask-Limiter</th>
|
||||
<td>3.12</td>
|
||||
<td>4.1.1</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6137,7 +6096,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>pypdf</th>
|
||||
<td>6.4.2</td>
|
||||
<td>6.6.2</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6155,7 +6114,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>regex</th>
|
||||
<td>2024.11.6</td>
|
||||
<td>2026.1.15</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6179,7 +6138,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>Unidecode</th>
|
||||
<td>1.3.8</td>
|
||||
<td>1.4.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6203,7 +6162,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestBackupMetadataGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6233,7 +6192,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestCliGdrivedb</td>
|
||||
</tr>
|
||||
|
||||
@@ -6263,7 +6222,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestEbookConvertCalibreGDrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6293,7 +6252,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestEbookConvertGDriveKepubify</td>
|
||||
</tr>
|
||||
|
||||
@@ -6335,7 +6294,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestEditAuthorsGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6371,7 +6330,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestEditBooksOnGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6413,7 +6372,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestEmbedMetadataGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6443,7 +6402,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.190.0</td>
|
||||
<td>TestSetupGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6496,8 +6455,8 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Flask-SimpleLDAP</th>
|
||||
<td>2.0.0</td>
|
||||
<th>flask-simpleldap</th>
|
||||
<td>2.1.0</td>
|
||||
<td>TestLdapLogin</td>
|
||||
</tr>
|
||||
|
||||
@@ -6545,7 +6504,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
</div>
|
||||
|
||||
<script>
|
||||
drawCircle(528, 2, 1, 7);
|
||||
drawCircle(530, 1, 0, 7);
|
||||
showCase(5);
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user