From 9d9748a218c10cde101ea7b630f630b6272fa682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Mon, 18 Mar 2024 23:06:09 +0100 Subject: [PATCH] Use pathlib, fix ruff issues --- store/__init__.py | 1 + store/app.py | 104 +++++++++++++++++++++-------------------- store/gunicorn.py | 11 +++-- store/requirements.txt | 9 ---- store/utils.py | 92 +++++++++++++++++++----------------- 5 files changed, 110 insertions(+), 107 deletions(-) delete mode 100644 store/requirements.txt diff --git a/store/__init__.py b/store/__init__.py index e69de29b..e5a0d9b4 100644 --- a/store/__init__.py +++ b/store/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/store/app.py b/store/app.py index 812d2b41..7d5c30d9 100644 --- a/store/app.py +++ b/store/app.py @@ -1,45 +1,39 @@ -import time -import re -import toml +#!/usr/bin/env python3 + import base64 import hashlib import hmac -import os -import string import random -import urllib -import json +import re +import string import sys -from slugify import slugify -from flask import ( - Flask, - send_from_directory, - render_template, - session, - redirect, - request, -) -from flask_babel import Babel +import time +import urllib +from pathlib import Path + +import toml +from flask import Flask, redirect, render_template, request, send_from_directory, session +from flask.typing import ResponseReturnValue +from flask_babel import Babel # type: ignore from flask_babel import gettext as _ from github import Github, InputGitAuthor +from slugify import slugify -sys.path = [os.path.dirname(__file__)] + sys.path - -from utils import ( - get_locale, - get_catalog, - get_wishlist, - get_stars, - get_app_md_and_screenshots, - save_wishlist_submit_for_ratelimit, +from .utils import ( 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") try: - config = toml.loads(open("config.toml").read()) -except Exception as e: + config = toml.loads(Path("config.toml").open().read()) +except RuntimeError: print( "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/") def app_info(app_id): infos = get_catalog()["apps"].get(app_id) - app_folder = os.path.join(config["APPS_CACHE"], app_id) - if not infos or not os.path.exists(app_folder): + app_folder = Path(config["APPS_CACHE"]) / app_id + if not infos or not app_folder.exists(): return f"App {app_id} not found", 404 get_app_md_and_screenshots(app_folder, infos) @@ -144,7 +138,7 @@ def app_info(app_id): @app.route("/app//") -def star_app(app_id, action): +def star_app(app_id: str, action) -> ResponseReturnValue: assert action in ["star", "unstar"] 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 @@ -153,26 +147,23 @@ def star_app(app_id, action): _("You must be logged in to be able to star an app") + "

" + _( - "Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.

'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." + "

" + "'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, ) - app_star_folder = os.path.join(".stars", app_id) - app_star_for_this_user = os.path.join( - ".stars", app_id, session.get("user", {})["id"] - ) + app_star_folder = Path(".stars") / app_id + app_star_for_this_user = app_star_folder / (session.get("user", {})["id"]) - if not os.path.exists(app_star_folder): - os.mkdir(app_star_folder) + app_star_folder.mkdir(exist_ok=True) if action == "star": - open(app_star_for_this_user, "w").write("") + app_star_for_this_user.open("w").write("") elif action == "unstar": - try: - os.remove(app_star_for_this_user) - except FileNotFoundError: - pass + app_star_folder.unlink(missing_ok=True) if app_id in get_catalog()["apps"]: 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") + "

" + _( - "Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.

'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." + "

'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( @@ -258,7 +252,8 @@ def add_to_wishlist(): check_wishlist_submit_ratelimit(session["user"]["username"]) is True 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")), @@ -298,7 +293,8 @@ def add_to_wishlist(): 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, successmsg=None, errormsg=_( - "An entry with the name %(slug)s already exists in the wishlist, instead, you can add a star to the app to show your interest.", + "An entry with the name %(slug)s already exists in the wishlist, instead, " + "you can add a star to the app to show your interest.", slug=slug, url=url, ), @@ -362,12 +359,13 @@ def add_to_wishlist(): # Get the commit base for the new branch, and create it commit_sha = repo.get_branch(repo.default_branch).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(e) url = "https://github.com/YunoHost/apps/pulls?q=is%3Apr+is%3Aopen+wishlist" errormsg = _( - "Failed to create the pull request to add the app to the wishlist… Maybe there's already a waiting PR for this app? 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 waiting PR for this app? Else, please report the issue to the YunoHost team.", url=url, ) return render_template( @@ -419,7 +417,8 @@ Description: {description} url = f"https://github.com/YunoHost/apps/pull/{pr.number}" successmsg = _( - "Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. You can track progress here: %(url)s", + "Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. " + "You can track progress here: %(url)s", url=url, ) @@ -495,7 +494,10 @@ def sso_login_callback(): _("Unfortunately, login was denied.") + "

" + _( - "Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.

'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." + "

'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, ) diff --git a/store/gunicorn.py b/store/gunicorn.py index 31b9b4e2..5dcbde69 100644 --- a/store/gunicorn.py +++ b/store/gunicorn.py @@ -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" -pythonpath = install_dir +pythonpath = str(install_dir) workers = 4 user = "appstore" bind = f"unix:{install_dir}/sock" pid = "/run/gunicorn/appstore-pid" errorlog = "/var/log/appstore/error.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" capture_output = True diff --git a/store/requirements.txt b/store/requirements.txt deleted file mode 100644 index d572e041..00000000 --- a/store/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Flask==2.3.2 -python-slugify -PyGithub -toml -pycmarkgfm -gunicorn -emoji -Babel -Flask-Babel diff --git a/store/utils.py b/store/utils.py index cf418421..a4182e0a 100644 --- a/store/utils.py +++ b/store/utils.py @@ -1,15 +1,22 @@ -import time +#!/usr/bin/env python3 + import base64 -import os import json -import toml +import os import subprocess +import time +from hashlib import md5 +from pathlib import Path + import pycmarkgfm +import tomlkit from emoji import emojize 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(): @@ -19,12 +26,13 @@ def get_locale(): def get_catalog(): - path = "../builds/default/v3/apps.json" - mtime = os.path.getmtime(path) + path = Path("../builds/default/v3/apps.json").resolve() + + mtime = path.stat().st_mtime if 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["antifeatures"] = {c["id"]: c for c in catalog["antifeatures"]} @@ -58,11 +66,11 @@ get_catalog() def get_wishlist(): - path = "../wishlist.toml" - mtime = os.path.getmtime(path) + path = Path("../wishlist.toml").resolve() + mtime = path.stat().st_mtime if 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 @@ -96,26 +104,22 @@ get_stars() def check_wishlist_submit_ratelimit(user): - dir_ = os.path.join(".wishlist_ratelimit") - if not os.path.exists(dir_): - os.mkdir(dir_) + dir_ = Path(".wishlist_ratelimit").resolve() + dir_.mkdir(exist_ok=True) + f = dir_ / md5(user.encode()).hexdigest() - f = os.path.join(dir_, md5(user.encode()).hexdigest()) - - return not os.path.exists(f) or (time.time() - os.path.getmtime(f)) > ( + return not f.exists() or (time.time() - f.stat().st_mtime) > ( 15 * 24 * 3600 ) # 15 days def save_wishlist_submit_for_ratelimit(user): - dir_ = os.path.join(".wishlist_ratelimit") - if not os.path.exists(dir_): - os.mkdir(dir_) + dir_ = Path(".wishlist_ratelimit").resolve() + dir_.mkdir(exist_ok=True) - f = os.path.join(dir_, md5(user.encode()).hexdigest()) - - open(f, "w").write("") + f = dir_ / md5(user.encode()).hexdigest() + f.touch() def human_to_binary(size: str) -> int: @@ -133,7 +137,7 @@ def human_to_binary(size: str) -> int: try: size_ = float(size) 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]) @@ -141,46 +145,48 @@ def human_to_binary(size: str) -> int: def get_app_md_and_screenshots(app_folder, infos): locale = get_locale() - if locale != "en" and os.path.exists( - os.path.join(app_folder, "doc", f"DESCRIPTION_{locale}.md") - ): - description_path = os.path.join(app_folder, "doc", f"DESCRIPTION_{locale}.md") - elif os.path.exists(os.path.join(app_folder, "doc", "DESCRIPTION.md")): - description_path = os.path.join(app_folder, "doc", "DESCRIPTION.md") + description_path_localized = app_folder / "doc" / f"DESCRIPTION_{locale}.md" + description_path_generic = app_folder / "doc" / "DESCRIPTION.md" + + if locale != "en" and description_path_localized.exists(): + description_path = description_path_localized + elif description_path_generic.exists(): + description_path = description_path_generic else: description_path = None if description_path: - with open(description_path) as f: + with description_path.open() as f: infos["full_description_html"] = emojize( pycmarkgfm.gfm_to_html(f.read()), language="alias" ) else: infos["full_description_html"] = infos["manifest"]["description"][locale] - if locale != "en" and os.path.exists( - os.path.join(app_folder, "doc", f"PRE_INSTALL_{locale}.md") - ): - pre_install_path = os.path.join(app_folder, "doc", f"PRE_INSTALL_{locale}.md") - elif os.path.exists(os.path.join(app_folder, "doc", "PRE_INSTALL.md")): - pre_install_path = os.path.join(app_folder, "doc", "PRE_INSTALL.md") + preinstall_path_localized = app_folder / "doc" / f"PRE_INSTALL_{locale}.md" + preinstall_path_generic = app_folder / "doc" / "PRE_INSTALL.md" + + if locale != "en" and preinstall_path_localized.exists(): + pre_install_path = preinstall_path_localized + elif preinstall_path_generic.exists(): + pre_install_path = preinstall_path_generic else: pre_install_path = None if pre_install_path: - with open(pre_install_path) as f: + with pre_install_path.open() as f: infos["pre_install_html"] = emojize( pycmarkgfm.gfm_to_html(f.read()), language="alias" ) infos["screenshot"] = None - screenshots_folder = os.path.join(app_folder, "doc", "screenshots") + screenshots_folder = app_folder / "doc" / "screenshots" - if os.path.exists(screenshots_folder): - with os.scandir(screenshots_folder) as it: + if screenshots_folder.exists(): + with screenshots_folder.iterdir() as 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"): - with open(entry.path, "rb") as img_file: + with entry.open("rb") as img_file: data = base64.b64encode(img_file.read()).decode("utf-8") infos["screenshot"] = ( f"data:image/{ext};charset=utf-8;base64,{data}"