From d70de9bd02ecd6b0ec86416f5f5f6a8844bc9012 Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Sat, 23 Dec 2023 04:24:26 +0100 Subject: [PATCH] Generate Atom feeds for updated apps Adds /pkgs/updates.atom and /apps/updates.atom for updated applications. This is a basic implementation. In the future, the entries could contain the application changelogs for example. Some refractoring had to be done as well related to generating download URLs, to avoid code duplication. --- chumweb/atom_feed.py | 176 +++++++++++++++++++++ chumweb/config.py | 4 + chumweb/package.py | 32 +++- chumweb/repo_loader.py | 6 +- chumweb/static_site_gen.py | 47 ++++-- chumweb/www/static/img/sailfishos-chum.png | Bin 0 -> 29360 bytes chumweb/www/views/layouts/base.html | 3 + chumweb/www/views/pages/package.html | 2 +- 8 files changed, 249 insertions(+), 21 deletions(-) create mode 100644 chumweb/atom_feed.py create mode 100644 chumweb/www/static/img/sailfishos-chum.png diff --git a/chumweb/atom_feed.py b/chumweb/atom_feed.py new file mode 100644 index 0000000..dc5d317 --- /dev/null +++ b/chumweb/atom_feed.py @@ -0,0 +1,176 @@ +""" +This package contains methods for writing Atom feeds +""" +from datetime import datetime +from typing import List, Optional, Iterable +from xml.dom.minidom import Document, Element + +from chumweb import CONFIG +from chumweb.package import Package + +# Reuse the namespace that the primary.xml.gz file uses +REPO_NS = "http://linux.duke.edu/metadata/common" + + +def create_atom_feed(public_url: str, title: str, updated: datetime) -> Document: + """ + Creates a basic Atom feed, with no entries + + https://validator.w3.org/feed/docs/atom.html + + :return: The created feed as an XML Document + """ + doc = Document() + feed = doc.createElementNS("http://www.w3.org/2005/Atom", "feed") + feed.setAttribute("xmlns", "http://www.w3.org/2005/Atom") + feed.setAttribute("xmlns:repo", "http://linux.duke.edu/metadata/common") + doc.appendChild(feed) + + el_id = _create_simple_element(doc, "id", public_url) + feed.appendChild(el_id) + + el_title = _create_simple_element(doc, "title", title) + feed.appendChild(el_title) + + el_updated = _create_simple_element(doc, "updated", updated.isoformat()) + feed.appendChild(el_updated) + + el_icon = _create_simple_element(doc, "icon", CONFIG.public_url + "static/img/sailfishos-chum.png") + feed.appendChild(el_icon) + + feed.appendChild(_create_link_el(doc, public_url, rel="self")) + + return doc + + +def create_package_atom_feed(pkgs: List[Package], public_url: str, title: str) -> Document: + """ + Creates a Atom feed with packages + :return: An XML Document representing the feed + """ + doc = create_atom_feed(public_url, title, pkgs[0].updated) + feed = doc.getElementsByTagName("feed")[0] + + for pkg in pkgs: + feed.appendChild(_create_pkg_entry(doc, pkg)) + + return doc + + +def _create_pkg_entry(doc: Document, pkg: Package) -> Element: + """ + Create a single entry for a package in an Atom feed + :param doc: The document where the elements should be created in + :param pkg: The package to create the entry for + :return: An element representing the package + """ + entry = doc.createElement("entry") + + entry_id = _create_simple_element(doc, "id", CONFIG.public_url + pkg.web_url()) + entry.appendChild(entry_id) + + entry_updated = _create_simple_element(doc, "updated", pkg.updated.isoformat()) + entry.appendChild(entry_updated) + + entry_title = _create_simple_element(doc, "title", pkg.title) + entry.appendChild(entry_title) + + entry_link = _create_link_el(doc, CONFIG.public_url + pkg.web_url()) + entry.appendChild(entry_link) + + entry_content_text = f"Package {pkg.name} was updated to version {pkg.version.to_short_str()}" + entry_content = _create_simple_element(doc, "content", entry_content_text, type="text") + entry.appendChild(entry_content) + + # Add author names + author_names = [] + + if pkg.packager_name: + author_names += [pkg.packager_name] + if pkg.developer_name: + author_names += [pkg.developer_name] + + for name in author_names: + entry_author = _create_simple_element(doc, "author") + entry_author_name = _create_simple_element(doc, "name", name) + entry_author.appendChild(entry_author_name) + entry.appendChild(entry_author) + + # Add categories + for category in pkg.categories: + entry_category = _create_simple_element(doc, "category", term=category) + entry.appendChild(entry_category) + + # Add download links for RPM files + for arch in pkg.archs: + download_size = pkg.download_size[arch] + entry_rpm_link = _create_link_el(doc, pkg.get_download_url(arch), rel="enclosure", + length=download_size, + title=f"{pkg.name}-{pkg.version.to_full_str()}-{arch}.rpm", + type="application/x-rpm") + entry.appendChild(entry_rpm_link) + + # Add chum-related metadata + for arch in pkg.archs: + pkg_el = _create_simple_element(doc, "repo:package", ns=REPO_NS, type="rpm") + + entry_chum_name = _create_simple_element(doc, "repo:name", pkg.name, REPO_NS) + pkg_el.appendChild(entry_chum_name) + + entry_chum_arch = _create_simple_element(doc, "repo:arch", arch, ns=REPO_NS) + pkg_el.appendChild(entry_chum_arch) + + entry_chum_version = _create_simple_element(doc, "repo:version", ns=REPO_NS, + epoch=pkg.version.epoch, ver=pkg.version.ver, rel=pkg.version.rel) + pkg_el.appendChild(entry_chum_version) + + entry_chum_summary = _create_simple_element(doc, "repo:summary", pkg.summary, ns=REPO_NS) + pkg_el.appendChild(entry_chum_summary) + + entry_chum_description = _create_simple_element(doc, "repo:description", pkg.description, ns=REPO_NS) + pkg_el.appendChild(entry_chum_description) + + entry_chum_url = _create_simple_element(doc, "repo:url", pkg.url, ns=REPO_NS) + pkg_el.appendChild(entry_chum_url) + + entry.appendChild(pkg_el) + + return entry + + +def _create_simple_element(doc: Document, tag_name: str, content: Optional[str | Element | Iterable[Element]] = None, + ns: Optional[str] = None, **attrs) -> Element: + """ + Creates a XML tag with the given tag name, children and attributes + :param tag_name: The name of the tag + :param content: The content of the tag + :param attrs: The attributes to set on the tag + :return: The created tag + """ + if ns: + el = doc.createElementNS(ns, tag_name) + else: + el = doc.createElement(tag_name) + + if content is None: + # Okay, do nothing + pass + elif type(content) is str: + el.appendChild(doc.createTextNode(content)) + elif type(content) is Element: + el.appendChild(content) + elif type(content) is Iterable[Element]: + for child in content: + el.appendChild(child) + else: + assert False, "Unsupported content type: " + str(type(content)) + + for key, value in attrs.items(): + el.setAttribute(key, value) + + return el + + +def _create_link_el(doc: Document, href: str, **kwargs): + kwargs["href"] = href + return _create_simple_element(doc, "link", **kwargs) diff --git a/chumweb/config.py b/chumweb/config.py index 4501258..016a2a3 100644 --- a/chumweb/config.py +++ b/chumweb/config.py @@ -48,8 +48,12 @@ class Config: repo_data_dir: str | None = None user_agent: str = "chumweb/1.0" source_code_url: str = "" + # The amount of featured apps to show on the home page featured_apps_count = 10 + # The amount of updated apps to show in the sidebar updated_apps_count = 6 + # The amount of updated apps to put in the Atom feed + feed_updated_apps_count = 20 def init_config() -> Config: diff --git a/chumweb/package.py b/chumweb/package.py index 2f76032..5e38562 100644 --- a/chumweb/package.py +++ b/chumweb/package.py @@ -1,17 +1,20 @@ """ Data classes for package metadata. It is also responsible for parsing the metadate of a single package """ +import logging from dataclasses import dataclass, field import enum -from datetime import datetime +from datetime import datetime, UTC from enum import StrEnum from types import NoneType -from typing import List, Dict, Self, Set +from typing import List, Dict, Self, Set, Optional from markupsafe import Markup +from . import CONFIG from .remote_image import RemoteImage +logger = logging.getLogger(__name__) class PackageApplicationCategory(StrEnum): """ @@ -97,6 +100,7 @@ class Package: debug_yaml_errors: List[Exception] = field(default_factory=list) updated: datetime | None = field(default_factory=lambda: datetime.fromtimestamp(0)) + repos: Set[str] = field(default_factory=set) archs: Set[str] = field(default_factory=set) download_size: Dict[str, int] = field(default_factory=dict) install_size: Dict[str, int] = field(default_factory=dict) @@ -105,7 +109,7 @@ class Package: checksum_value: Dict[str, str] = field(default_factory=dict) @staticmethod - def from_node(dom_element): + def from_node(dom_element, repo_arch: str): """ Creates a Package class instance from a `` XML node `dom_element` as found in the primary.xml metadata in RPM repositories. @@ -210,13 +214,14 @@ def parse_description(description: str, name: str): arch = try_get_str("arch") p = Package(try_get_str("name")) + p.repos.add(repo_arch) p.archs.add(arch) p.summary = try_get_str("summary") p.version = try_get_version() p.url = try_get_str("url") p.title = name_to_title(p.name) p.licence = try_get_str("rpm:license") - p.updated = datetime.fromtimestamp(float(try_get_attribute_tags("time", "file")[0])) + p.updated = datetime.fromtimestamp(float(try_get_attribute_tags("time", "file")[0]), UTC) p.download_size[arch], p.install_size[arch] = try_get_attribute_tags("size", "package", "installed") p.download_url[arch] = try_get_attribute_tags("location", "href")[0] @@ -239,6 +244,7 @@ def merge_arch(self, other_pkg: Self): Adds the architecture-specific information from another package to this package """ for arch in other_pkg.archs: + self.repos = self.repos.union(other_pkg.repos) self.download_size[arch] = other_pkg.download_size[arch] self.install_size[arch] = other_pkg.install_size[arch] self.download_url[arch] = other_pkg.download_url[arch] @@ -266,6 +272,24 @@ def web_url(self): else: return f"pkgs/{self.name}/" + def get_download_url(self, arch: str) -> Optional[str]: + # noarch does not have a dedicated repository, use the first available arch I suppose + # This may be an idea in the category "not smart" + if arch == "noarch": + repo = next(self.repos.__iter__()) + else: + for repo in self.repos: + repo_arch = repo.split("_")[1] + if repo_arch == arch: + break + else: + logger.warning(f"No repo found for architecture {arch} (package: {self.name})") + #assert False, f"No repo found for architecture {arch} (package: {self.name})" + return None + + return f"{CONFIG.repo_url_prefix}{repo}/" + self.download_url[arch] + + def caused_requests(self): return type(self.markdown_url) == str diff --git a/chumweb/repo_loader.py b/chumweb/repo_loader.py index 74a45f9..442a876 100644 --- a/chumweb/repo_loader.py +++ b/chumweb/repo_loader.py @@ -115,7 +115,7 @@ def load_repo(obs_url: str, obs_project: str, obs_auth: Tuple[str, str], repo_ur for data_path, repo_name in zip(data_paths, repos): step_progress(parse_step, repo_name, i, repo_count) arch = repo_name.split("_")[1] - all_pkgs[arch] = read_repo_data(urljoin(repo_url, repo_name), data_path) + all_pkgs[arch] = read_repo_data(urljoin(repo_url, repo_name), data_path, repo_name) link_debug_packages(all_pkgs[arch]) i += 1 @@ -171,7 +171,7 @@ def save_repo_data(repo_url: str, repo_name: str, out_dir: Path): return primary_gz_path -def read_repo_data(repo_url, repo_info: Path) -> List[Package]: +def read_repo_data(repo_url, repo_info: Path, repo_name: str) -> List[Package]: """ Reads all package data from a `primary.xml.gz` file """ @@ -179,7 +179,7 @@ def read_repo_data(repo_url, repo_info: Path) -> List[Package]: with GzipFile(repo_info) as gz: xml = minidom.parse(gz) for xmlPkg in xml.getElementsByTagName("package"): - pkgs.append(Package.from_node(xmlPkg)) + pkgs.append(Package.from_node(xmlPkg, repo_name)) return pkgs diff --git a/chumweb/static_site_gen.py b/chumweb/static_site_gen.py index 7d0cb69..86ec547 100644 --- a/chumweb/static_site_gen.py +++ b/chumweb/static_site_gen.py @@ -17,6 +17,7 @@ from typing import List, Dict, Tuple, Set from . import CONFIG +from .atom_feed import create_package_atom_feed from .package import Package, PackageApplicationCategory from datetime import datetime @@ -56,6 +57,17 @@ def __hash__(self): return self.name.__hash__() +@dataclass +class Feed: + title: str + path: str + pkgs: List[Package] + + def __getattr__(self, item): + if item == "url": + return CONFIG.public_url + self.path + + CATEGORY_PAGES = [ CategoryPage("Accessibility", {PackageApplicationCategory.accessibility}), CategoryPage("Development", {PackageApplicationCategory.development}), @@ -64,7 +76,8 @@ def __hash__(self): CategoryPage("Graphics", {PackageApplicationCategory.graphics}), CategoryPage("Libraries", {PackageApplicationCategory.library}), CategoryPage("Location and Navigation", {PackageApplicationCategory.maps}), - CategoryPage("Multimedia", {PackageApplicationCategory.audio, PackageApplicationCategory.video, PackageApplicationCategory.audio_video}), + CategoryPage("Multimedia", {PackageApplicationCategory.audio, PackageApplicationCategory.video, + PackageApplicationCategory.audio_video}), CategoryPage("Office", {PackageApplicationCategory.office}), CategoryPage("Science", {PackageApplicationCategory.science}), CategoryPage("Utilities", {PackageApplicationCategory.system, PackageApplicationCategory.utility}), @@ -85,25 +98,28 @@ def gen_site(repo_info: RepoInfo, out_dir: Path): updated = datetime.today() pkg_filters = ["pkgs", "apps"] - repo_url_prefixes = {arch: CONFIG.repo_url_prefix + repo + "/" for repo, arch in - zip(repo_info.repos, repo_info.repo_archs())} - # noarch does not have a dedicated repository, use the first available arch I suppose - # This may be an idea in the category "not smart" - repo_url_prefixes["noarch"] = repo_url_prefixes[repo_info.repo_archs()[0]] - sorted_pkgs = sorted([pkg for pkg in repo_info.packages if not pkg.is_debug()], key=lambda pkg: str(pkg.title).lower()) - recently_updated_pkgs = sorted( + recently_updated_apps = sorted( [pkg for pkg in repo_info.packages if pkg.is_app()], reverse=True, key=lambda pkg: pkg.updated - )[:CONFIG.updated_apps_count] + ) + recently_updated_pkgs = sorted( + [pkg for pkg in repo_info.packages if not pkg.is_debug()], + reverse=True, key=lambda pkg: pkg.updated + ) + feeds = [ + Feed("Recently updated apps", "apps/updates.atom", recently_updated_apps), + Feed("Recently updated packages", "pkgs/updates.atom", recently_updated_pkgs) + ] def render_template(template: Template, out_file: str | Path, **kwargs): kwargs["updated"] = updated kwargs["chum_installer"] = "sailfishos-chum-gui-installer" kwargs["config"] = CONFIG kwargs["repo_version"] = repo_info.version - kwargs["recently_updated_pkgs"] = recently_updated_pkgs + kwargs["recently_updated_pkgs"] = recently_updated_pkgs[:CONFIG.updated_apps_count] + kwargs["feeds"] = feeds template.stream(**kwargs).dump(str(out_file)) def _copy_dir(source, dest: Path): @@ -250,14 +266,13 @@ def create_package_page(pkg: Package): app_dir = www_apps_path.joinpath(pkg.name) os.symlink(pkg_dir.absolute(), app_dir.absolute(), True) - render_template(pkg_template, str(out_file), pkg=pkg, repo_url_prefixes=repo_url_prefixes) + render_template(pkg_template, str(out_file), pkg=pkg) total_sitegen_steps = 5 step_progress(sitegen_step, "Creating directory structure", 1, total_sitegen_steps) recreate_directory_skeleton() copy_static_dirs() - env = Environment( loader=PackageLoader(__package__ + ".www", "views"), autoescape=select_autoescape(), @@ -303,10 +318,16 @@ def create_package_page(pkg: Package): with open(www_path.joinpath("packages.json"), "w") as packages_file: json.dump(search_documents, packages_file) + # Write Atom feeds + for feed in feeds: + xml = create_package_atom_feed(feed.pkgs[:CONFIG.feed_updated_apps_count], feed.url, feed.title) + with open(www_path.joinpath(feed.path), "w") as atom_file: + xml.writexml(atom_file) + def _bytes_filter(size: str) -> str: """ - Converts `size` in bytes to a human readable unit, such as KiB, MiB and GiB + Converts `size` in bytes to a human-readable unit, such as KiB, MiB and GiB """ from math import log2 diff --git a/chumweb/www/static/img/sailfishos-chum.png b/chumweb/www/static/img/sailfishos-chum.png new file mode 100644 index 0000000000000000000000000000000000000000..98680e6307e48c6021556a2e4032075ca4149946 GIT binary patch literal 29360 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 literal 0 HcmV?d00001 diff --git a/chumweb/www/views/layouts/base.html b/chumweb/www/views/layouts/base.html index d7d51f8..756dc05 100644 --- a/chumweb/www/views/layouts/base.html +++ b/chumweb/www/views/layouts/base.html @@ -15,6 +15,9 @@ {% endblock header %} + {% for feed in feeds %} + + {% endfor %}
diff --git a/chumweb/www/views/pages/package.html b/chumweb/www/views/pages/package.html index 3d805a3..c6453e8 100644 --- a/chumweb/www/views/pages/package.html +++ b/chumweb/www/views/pages/package.html @@ -56,7 +56,7 @@

{{ pkg.title }}

{% endif %} {% if pkg.name not in config.chum_installer_pkgs %}