diff --git a/frappe/www/attribution.html b/frappe/www/attribution.html new file mode 100644 index 0000000000..efeed6024c --- /dev/null +++ b/frappe/www/attribution.html @@ -0,0 +1,42 @@ +{% extends "templates/web.html" %} + +{% block page_content %} + +

{{ _("Attribution") }}

+

+ {{ _("This software is built on top of many open source packages. We would like to thank the authors of these packages for their contribution.") }} +

+ +{% for app, packages in packages_by_app.items() %} +
+

{{ app }}

+ + + + + + + + + + + {% for package in packages %} + + + + + + + {% endfor %} + +
{{ _("Package") }}{{ _("Version") }}{{ _("License") }}{{ _("Author") }}
+ {% if package.homepage %} + {{ package.name | e }} + {% else %} + {{ package.name | e }} + {% endif %} + {{ package.version | e }}{{ (package.license | e) or _("Unknown") }}{{ (package.author | e) or _("Unknown") }}
+
+{% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/frappe/www/attribution.py b/frappe/www/attribution.py new file mode 100644 index 0000000000..bfc8f2eda7 --- /dev/null +++ b/frappe/www/attribution.py @@ -0,0 +1,78 @@ +import json +import re +import tomllib + +import requests + +import frappe + + +def get_context(context): + packages_by_app = {} + for app in frappe.get_installed_apps(): + packages_by_app[app] = get_app_deps(app) + + context.packages_by_app = packages_by_app + + +def get_app_deps(app: str): + dependencies = [] + + app_info = get_pyproject_info(app) + for requirement in app_info.get("dependencies", []): + name, version = parse_pip_requirement(requirement) + info = get_py_registry_info(name) + info["name"] = name + info["version"] = version + dependencies.append(info) + + for name, version in get_js_deps(app).items(): + info = get_js_registry_info(name) + info["name"] = name + info["version"] = version + dependencies.append(info) + + return dependencies + + +def get_js_deps(app: str): + package_json = frappe.get_app_path(app, "..", "package.json") + with open(package_json) as f: + package = json.load(f) + + return package.get("dependencies", {}) + + +def get_js_registry_info(package): + registry_url = f"https://registry.npmjs.org/{package}" + registry_info = requests.get(registry_url).json() + return { + "license": registry_info.get("license"), + "author": registry_info.get("author", {}).get("name"), + "homepage": registry_info.get("homepage"), + } + + +def get_pyproject_info(app: str) -> list[str]: + pyproject_toml = frappe.get_app_path(app, "..", "pyproject.toml") + with open(pyproject_toml, "rb") as f: + pyproject = tomllib.load(f) + + return pyproject.get("project", {}) + + +def get_py_registry_info(package): + registry_url = f"https://pypi.org/pypi/{package}/json" + registry_info = requests.get(registry_url).json().get("info", {}) + return { + "license": registry_info.get("license"), + "author": registry_info.get("author"), + "homepage": registry_info.get("home_page"), + } + + +def parse_pip_requirement(requirement: str) -> tuple[str, str]: + """Parse pip requirement string to package name and version""" + match = re.match(r"^([A-Za-z0-9_\-\[\]]+)(.*)$", requirement) + + return (match[1], match[2]) if match else (requirement, "")