1
0
Fork 0
mirror of https://github.com/YunoHost/apps.git synced 2024-09-03 20:06:07 +02:00

Use pathlib, fix ruff issues

This commit is contained in:
Salamandar 2024-03-18 23:06:09 +01:00
parent b44b882474
commit 9d9748a218
5 changed files with 110 additions and 107 deletions

View file

@ -0,0 +1 @@
#!/usr/bin/env python3

View file

@ -1,45 +1,39 @@
import time #!/usr/bin/env python3
import re
import toml
import base64 import base64
import hashlib import hashlib
import hmac import hmac
import os
import string
import random import random
import urllib import re
import json import string
import sys import sys
from slugify import slugify import time
from flask import ( import urllib
Flask, from pathlib import Path
send_from_directory,
render_template, import toml
session, from flask import Flask, redirect, render_template, request, send_from_directory, session
redirect, from flask.typing import ResponseReturnValue
request, from flask_babel import Babel # type: ignore
)
from flask_babel import Babel
from flask_babel import gettext as _ from flask_babel import gettext as _
from github import Github, InputGitAuthor from github import Github, InputGitAuthor
from slugify import slugify
sys.path = [os.path.dirname(__file__)] + sys.path from .utils import (
from utils import (
get_locale,
get_catalog,
get_wishlist,
get_stars,
get_app_md_and_screenshots,
save_wishlist_submit_for_ratelimit,
check_wishlist_submit_ratelimit, check_wishlist_submit_ratelimit,
get_app_md_and_screenshots,
get_catalog,
get_locale,
get_stars,
get_wishlist,
save_wishlist_submit_for_ratelimit,
) )
app = Flask(__name__, static_url_path="/assets", static_folder="assets") app = Flask(__name__, static_url_path="/assets", static_folder="assets")
try: try:
config = toml.loads(open("config.toml").read()) config = toml.loads(Path("config.toml").open().read())
except Exception as e: except RuntimeError:
print( print(
"You should create a config.toml with the appropriate key/values, cf config.toml.example" "You should create a config.toml with the appropriate key/values, cf config.toml.example"
) )
@ -126,8 +120,8 @@ def popularity_json():
@app.route("/app/<app_id>") @app.route("/app/<app_id>")
def app_info(app_id): def app_info(app_id):
infos = get_catalog()["apps"].get(app_id) infos = get_catalog()["apps"].get(app_id)
app_folder = os.path.join(config["APPS_CACHE"], app_id) app_folder = Path(config["APPS_CACHE"]) / app_id
if not infos or not os.path.exists(app_folder): if not infos or not app_folder.exists():
return f"App {app_id} not found", 404 return f"App {app_id} not found", 404
get_app_md_and_screenshots(app_folder, infos) get_app_md_and_screenshots(app_folder, infos)
@ -144,7 +138,7 @@ def app_info(app_id):
@app.route("/app/<app_id>/<action>") @app.route("/app/<app_id>/<action>")
def star_app(app_id, action): def star_app(app_id: str, action) -> ResponseReturnValue:
assert action in ["star", "unstar"] assert action in ["star", "unstar"]
if app_id not in get_catalog()["apps"] and app_id not in get_wishlist(): if app_id not in get_catalog()["apps"] and app_id not in get_wishlist():
return _("App %(app_id) not found", app_id=app_id), 404 return _("App %(app_id) not found", app_id=app_id), 404
@ -153,26 +147,23 @@ def star_app(app_id, action):
_("You must be logged in to be able to star an app") _("You must be logged in to be able to star an app")
+ "<br/><br/>" + "<br/><br/>"
+ _( + _(
"Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts." "Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users."
"<br/><br/>"
"'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: "
"entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts."
), ),
401, 401,
) )
app_star_folder = os.path.join(".stars", app_id) app_star_folder = Path(".stars") / app_id
app_star_for_this_user = os.path.join( app_star_for_this_user = app_star_folder / (session.get("user", {})["id"])
".stars", app_id, session.get("user", {})["id"]
)
if not os.path.exists(app_star_folder): app_star_folder.mkdir(exist_ok=True)
os.mkdir(app_star_folder)
if action == "star": if action == "star":
open(app_star_for_this_user, "w").write("") app_star_for_this_user.open("w").write("")
elif action == "unstar": elif action == "unstar":
try: app_star_folder.unlink(missing_ok=True)
os.remove(app_star_for_this_user)
except FileNotFoundError:
pass
if app_id in get_catalog()["apps"]: if app_id in get_catalog()["apps"]:
return redirect(f"/app/{app_id}") return redirect(f"/app/{app_id}")
@ -203,7 +194,10 @@ def add_to_wishlist():
_("You must be logged in to submit an app to the wishlist") _("You must be logged in to submit an app to the wishlist")
+ "<br/><br/>" + "<br/><br/>"
+ _( + _(
"Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts." "Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users."
"<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more "
"specifically: entering at least 5 topics, reading at least 30 posts, and spending at least "
"10 minutes reading posts."
) )
) )
return render_template( return render_template(
@ -258,7 +252,8 @@ def add_to_wishlist():
check_wishlist_submit_ratelimit(session["user"]["username"]) is True check_wishlist_submit_ratelimit(session["user"]["username"]) is True
and session["user"]["bypass_ratelimit"] is False, and session["user"]["bypass_ratelimit"] is False,
_( _(
"Proposing wishlist additions is limited to once every 15 days per user. Please try again in a few days." "Proposing wishlist additions is limited to once every 15 days per user. "
"Please try again in a few days."
), ),
), ),
(len(name) >= 3, _("App name should be at least 3 characters")), (len(name) >= 3, _("App name should be at least 3 characters")),
@ -298,7 +293,8 @@ def add_to_wishlist():
for keyword in boring_keywords_to_check_for_people_not_reading_the_instructions for keyword in boring_keywords_to_check_for_people_not_reading_the_instructions
), ),
_( _(
"Please focus on what the app does, without using marketing, fuzzy terms, or repeating that the app is 'free' and 'self-hostable'." "Please focus on what the app does, without using marketing, fuzzy terms, "
"or repeating that the app is 'free' and 'self-hostable'."
), ),
), ),
( (
@ -342,7 +338,8 @@ def add_to_wishlist():
csrf_token=csrf_token, csrf_token=csrf_token,
successmsg=None, successmsg=None,
errormsg=_( errormsg=_(
"An entry with the name %(slug)s already exists in the wishlist, instead, you can <a href='%(url)s'>add a star to the app to show your interest</a>.", "An entry with the name %(slug)s already exists in the wishlist, instead, "
"you can <a href='%(url)s'>add a star to the app to show your interest</a>.",
slug=slug, slug=slug,
url=url, url=url,
), ),
@ -362,12 +359,13 @@ def add_to_wishlist():
# Get the commit base for the new branch, and create it # Get the commit base for the new branch, and create it
commit_sha = repo.get_branch(repo.default_branch).commit.sha commit_sha = repo.get_branch(repo.default_branch).commit.sha
repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=commit_sha) repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=commit_sha)
except exception as e: except Exception as e:
print("… Failed to create branch ?") print("… Failed to create branch ?")
print(e) print(e)
url = "https://github.com/YunoHost/apps/pulls?q=is%3Apr+is%3Aopen+wishlist" url = "https://github.com/YunoHost/apps/pulls?q=is%3Apr+is%3Aopen+wishlist"
errormsg = _( errormsg = _(
"Failed to create the pull request to add the app to the wishlist… Maybe there's already <a href='%(url)s'>a waiting PR for this app</a>? Else, please report the issue to the YunoHost team.", "Failed to create the pull request to add the app to the wishlist… Maybe there's already "
"<a href='%(url)s'>a waiting PR for this app</a>? Else, please report the issue to the YunoHost team.",
url=url, url=url,
) )
return render_template( return render_template(
@ -419,7 +417,8 @@ Description: {description}
url = f"https://github.com/YunoHost/apps/pull/{pr.number}" url = f"https://github.com/YunoHost/apps/pull/{pr.number}"
successmsg = _( successmsg = _(
"Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. You can track progress here: <a href='%(url)s'>%(url)s</a>", "Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. "
"You can track progress here: <a href='%(url)s'>%(url)s</a>",
url=url, url=url,
) )
@ -495,7 +494,10 @@ def sso_login_callback():
_("Unfortunately, login was denied.") _("Unfortunately, login was denied.")
+ "<br/><br/>" + "<br/><br/>"
+ _( + _(
"Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts." "Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users."
"<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more "
"specifically: entering at least 5 topics, reading at least 30 posts, and spending at least "
"10 minutes reading posts."
), ),
403, 403,
) )

View file

@ -1,14 +1,17 @@
import os #!/usr/bin/env python3
install_dir = os.path.dirname(__file__) from pathlib import Path
install_dir = Path(__file__).resolve().parent
command = f"{install_dir}/venv/bin/gunicorn" command = f"{install_dir}/venv/bin/gunicorn"
pythonpath = install_dir pythonpath = str(install_dir)
workers = 4 workers = 4
user = "appstore" user = "appstore"
bind = f"unix:{install_dir}/sock" bind = f"unix:{install_dir}/sock"
pid = "/run/gunicorn/appstore-pid" pid = "/run/gunicorn/appstore-pid"
errorlog = "/var/log/appstore/error.log" errorlog = "/var/log/appstore/error.log"
accesslog = "/var/log/appstore/access.log" accesslog = "/var/log/appstore/access.log"
access_log_format = '%({X-Real-IP}i)s %({X-Forwarded-For}i)s %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' access_log_format = \
'%({X-Real-IP}i)s %({X-Forwarded-For}i)s %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
loglevel = "warning" loglevel = "warning"
capture_output = True capture_output = True

View file

@ -1,9 +0,0 @@
Flask==2.3.2
python-slugify
PyGithub
toml
pycmarkgfm
gunicorn
emoji
Babel
Flask-Babel

View file

@ -1,15 +1,22 @@
import time #!/usr/bin/env python3
import base64 import base64
import os
import json import json
import toml import os
import subprocess import subprocess
import time
from hashlib import md5
from pathlib import Path
import pycmarkgfm import pycmarkgfm
import tomlkit
from emoji import emojize from emoji import emojize
from flask import request from flask import request
from hashlib import md5
AVAILABLE_LANGUAGES = ["en"] + os.listdir("translations") TRANSLATIONS_DIR = Path(__file__).parent / "translations"
AVAILABLE_LANGUAGES = ["en"] + [str(d) for d in TRANSLATIONS_DIR.glob("*/")]
def get_locale(): def get_locale():
@ -19,12 +26,13 @@ def get_locale():
def get_catalog(): def get_catalog():
path = "../builds/default/v3/apps.json" path = Path("../builds/default/v3/apps.json").resolve()
mtime = os.path.getmtime(path)
mtime = path.stat().st_mtime
if get_catalog.mtime_catalog != mtime: if get_catalog.mtime_catalog != mtime:
get_catalog.mtime_catalog = mtime get_catalog.mtime_catalog = mtime
catalog = json.load(open(path)) catalog = json.load(path.open())
catalog["categories"] = {c["id"]: c for c in catalog["categories"]} catalog["categories"] = {c["id"]: c for c in catalog["categories"]}
catalog["antifeatures"] = {c["id"]: c for c in catalog["antifeatures"]} catalog["antifeatures"] = {c["id"]: c for c in catalog["antifeatures"]}
@ -58,11 +66,11 @@ get_catalog()
def get_wishlist(): def get_wishlist():
path = "../wishlist.toml" path = Path("../wishlist.toml").resolve()
mtime = os.path.getmtime(path) mtime = path.stat().st_mtime
if get_wishlist.mtime_wishlist != mtime: if get_wishlist.mtime_wishlist != mtime:
get_wishlist.mtime_wishlist = mtime get_wishlist.mtime_wishlist = mtime
get_wishlist.cache_wishlist = toml.load(open(path)) get_wishlist.cache_wishlist = tomlkit.load(path.open())
return get_wishlist.cache_wishlist return get_wishlist.cache_wishlist
@ -96,26 +104,22 @@ get_stars()
def check_wishlist_submit_ratelimit(user): def check_wishlist_submit_ratelimit(user):
dir_ = os.path.join(".wishlist_ratelimit") dir_ = Path(".wishlist_ratelimit").resolve()
if not os.path.exists(dir_): dir_.mkdir(exist_ok=True)
os.mkdir(dir_) f = dir_ / md5(user.encode()).hexdigest()
f = os.path.join(dir_, md5(user.encode()).hexdigest()) return not f.exists() or (time.time() - f.stat().st_mtime) > (
return not os.path.exists(f) or (time.time() - os.path.getmtime(f)) > (
15 * 24 * 3600 15 * 24 * 3600
) # 15 days ) # 15 days
def save_wishlist_submit_for_ratelimit(user): def save_wishlist_submit_for_ratelimit(user):
dir_ = os.path.join(".wishlist_ratelimit") dir_ = Path(".wishlist_ratelimit").resolve()
if not os.path.exists(dir_): dir_.mkdir(exist_ok=True)
os.mkdir(dir_)
f = os.path.join(dir_, md5(user.encode()).hexdigest()) f = dir_ / md5(user.encode()).hexdigest()
f.touch()
open(f, "w").write("")
def human_to_binary(size: str) -> int: def human_to_binary(size: str) -> int:
@ -133,7 +137,7 @@ def human_to_binary(size: str) -> int:
try: try:
size_ = float(size) size_ = float(size)
except Exception: except Exception:
raise Exception(f"Failed to convert size {size} to float") raise Exception(f"Failed to convert size {size} to float") # noqa: B904
return int(size_ * factor[suffix]) return int(size_ * factor[suffix])
@ -141,46 +145,48 @@ def human_to_binary(size: str) -> int:
def get_app_md_and_screenshots(app_folder, infos): def get_app_md_and_screenshots(app_folder, infos):
locale = get_locale() locale = get_locale()
if locale != "en" and os.path.exists( description_path_localized = app_folder / "doc" / f"DESCRIPTION_{locale}.md"
os.path.join(app_folder, "doc", f"DESCRIPTION_{locale}.md") description_path_generic = app_folder / "doc" / "DESCRIPTION.md"
):
description_path = os.path.join(app_folder, "doc", f"DESCRIPTION_{locale}.md") if locale != "en" and description_path_localized.exists():
elif os.path.exists(os.path.join(app_folder, "doc", "DESCRIPTION.md")): description_path = description_path_localized
description_path = os.path.join(app_folder, "doc", "DESCRIPTION.md") elif description_path_generic.exists():
description_path = description_path_generic
else: else:
description_path = None description_path = None
if description_path: if description_path:
with open(description_path) as f: with description_path.open() as f:
infos["full_description_html"] = emojize( infos["full_description_html"] = emojize(
pycmarkgfm.gfm_to_html(f.read()), language="alias" pycmarkgfm.gfm_to_html(f.read()), language="alias"
) )
else: else:
infos["full_description_html"] = infos["manifest"]["description"][locale] infos["full_description_html"] = infos["manifest"]["description"][locale]
if locale != "en" and os.path.exists( preinstall_path_localized = app_folder / "doc" / f"PRE_INSTALL_{locale}.md"
os.path.join(app_folder, "doc", f"PRE_INSTALL_{locale}.md") preinstall_path_generic = app_folder / "doc" / "PRE_INSTALL.md"
):
pre_install_path = os.path.join(app_folder, "doc", f"PRE_INSTALL_{locale}.md") if locale != "en" and preinstall_path_localized.exists():
elif os.path.exists(os.path.join(app_folder, "doc", "PRE_INSTALL.md")): pre_install_path = preinstall_path_localized
pre_install_path = os.path.join(app_folder, "doc", "PRE_INSTALL.md") elif preinstall_path_generic.exists():
pre_install_path = preinstall_path_generic
else: else:
pre_install_path = None pre_install_path = None
if pre_install_path: if pre_install_path:
with open(pre_install_path) as f: with pre_install_path.open() as f:
infos["pre_install_html"] = emojize( infos["pre_install_html"] = emojize(
pycmarkgfm.gfm_to_html(f.read()), language="alias" pycmarkgfm.gfm_to_html(f.read()), language="alias"
) )
infos["screenshot"] = None infos["screenshot"] = None
screenshots_folder = os.path.join(app_folder, "doc", "screenshots") screenshots_folder = app_folder / "doc" / "screenshots"
if os.path.exists(screenshots_folder): if screenshots_folder.exists():
with os.scandir(screenshots_folder) as it: with screenshots_folder.iterdir() as it:
for entry in it: for entry in it:
ext = os.path.splitext(entry.name)[1].replace(".", "").lower() ext = entry.suffix.lower()
if entry.is_file() and ext in ("png", "jpg", "jpeg", "webp", "gif"): if entry.is_file() and ext in ("png", "jpg", "jpeg", "webp", "gif"):
with open(entry.path, "rb") as img_file: with entry.open("rb") as img_file:
data = base64.b64encode(img_file.read()).decode("utf-8") data = base64.b64encode(img_file.read()).decode("utf-8")
infos["screenshot"] = ( infos["screenshot"] = (
f"data:image/{ext};charset=utf-8;base64,{data}" f"data:image/{ext};charset=utf-8;base64,{data}"