seitime-frappe/frappe/www/attribution.py
Akhil Narang 588cb1e44d
refactor: tomli -> tomllib
Signed-off-by: Akhil Narang <me@akhilnarang.dev>
2025-12-22 21:06:48 +05:30

145 lines
3.9 KiB
Python

import contextlib
import importlib.metadata
import json
import re
import tomllib
from pathlib import Path
import frappe
from frappe import _
from frappe.permissions import is_system_user
def get_context(context):
if not is_system_user():
frappe.throw(_("You need to be a system user to access this page."), frappe.PermissionError)
apps = []
for app in frappe.get_installed_apps():
app_info = get_app_info(app)
if any([app_info.get("authors"), app_info.get("dependencies"), app_info.get("description")]):
apps.append(app_info)
context.apps = apps
def get_app_info(app: str):
app_info = get_pyproject_info(app)
result = {
"name": app,
"description": app_info.get("description", ""),
"authors": ", ".join([a.get("name", "") for a in app_info.get("authors", [])]),
"dependencies": [],
}
for requirement in app_info.get("dependencies", []):
name = parse_pip_requirement(requirement)
metadata = get_python_package_metadata(name)
result["dependencies"].append(
{"name": name, "type": "Python", "license": metadata["license"], "author": metadata["author"]}
)
result["dependencies"].extend(get_js_deps(app))
return result
def get_python_package_metadata(package_name: str) -> dict:
"""Get metadata for a Python package using importlib.metadata"""
try:
metadata = importlib.metadata.metadata(package_name)
return {
"license": (
metadata.get("License-Expression")
or parse_classifiers(metadata.get_all("Classifier", []))
or metadata.get("License") # May contain a full license text, less preferred
or "Unknown"
),
"author": (
metadata.get("Author")
or metadata.get("Maintainer")
or metadata.get("Author-email")
or metadata.get("Maintainer-email")
or "Unknown"
),
}
except importlib.metadata.PackageNotFoundError:
return {"license": "Unknown", "author": "Unknown"}
def parse_classifiers(classifiers: list[str]) -> str | None:
"""Parse classifiers to get the license"""
for classifier in classifiers:
if classifier.startswith("License ::"):
return classifier.split("::")[-1].strip()
return None
def get_js_deps(app: str) -> list[dict]:
package_json = Path(frappe.get_app_path(app, "..", "package.json"))
if not package_json.exists():
return []
with open(package_json) as f:
package = json.load(f)
packages = package.get("dependencies", {}).keys()
result = []
# Get the node_modules directory
node_modules_path = Path(frappe.get_app_path(app, "..", "node_modules"))
for name in packages:
# Initialize with basic info
package_info = {"name": name, "type": "JavaScript", "license": "Unknown", "author": "Unknown"}
# Try to find package.json in node_modules
package_json_path = node_modules_path / name / "package.json"
if package_json_path.exists():
pkg_data = None
with contextlib.suppress(json.JSONDecodeError):
pkg_data = json.loads(package_json_path.read_text())
if not pkg_data:
continue
# Extract license info
license_info = pkg_data.get("license")
if isinstance(license_info, dict):
license_info = license_info.get("type")
if license_info:
package_info["license"] = license_info
# Extract author info
author = pkg_data.get("author")
if isinstance(author, dict):
author = author.get("name")
if not author:
maintainers = pkg_data.get("maintainers", [])
if maintainers:
author = ", ".join([m for m in maintainers if m])
if author:
package_info["author"] = author
result.append(package_info)
return result
def get_pyproject_info(app: str) -> dict:
pyproject_toml = Path(frappe.get_app_path(app, "..", "pyproject.toml"))
if not pyproject_toml.exists():
return {}
with open(pyproject_toml, "rb") as f:
pyproject = tomllib.load(f)
return pyproject.get("project", {})
def parse_pip_requirement(requirement: str) -> str:
"""Parse pip requirement string to package name and version"""
match = re.match(r"^([A-Za-z0-9_\-\[\]]+)(.*)$", requirement)
return match[1] if match else requirement