proj/pkgcore/pkgcore:master commit in: examples/

"Arthur Zamarin" <[email protected]>
Newsgroups gmane.linux.gentoo.cvs
Message-ID <1786214878.65e01698b887827a69b68e295c02b414187bf922.arthurzam@gentoo>
commit:     65e01698b887827a69b68e295c02b414187bf922
Author:     Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
AuthorDate: Sat Aug  8 10:35:40 2026 +0000
Commit:     Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Sat Aug  8 18:47:58 2026 +0000
URL:        https://gitweb.gentoo.org/proj/pkgcore/pkgcore.git/commit/?id=65e01698

examples: migrate existing scripts to new bugzilla module

Signed-off-by: Arthur Zamarin <arthurzam <AT> gentoo.org>

 examples/destable_arch_bugs.py | 103 ++++++++------------------
 examples/set_maintainers.py    | 116 +++++++++---------------------
 examples/verify_at_done.py     | 160 ++++++++++++++++++-----------------------
 3 files changed, 133 insertions(+), 246 deletions(-)

diff --git a/examples/destable_arch_bugs.py b/examples/destable_arch_bugs.py
index 00ca0290e..c8607343c 100755
--- a/examples/destable_arch_bugs.py
+++ b/examples/destable_arch_bugs.py
@@ -2,26 +2,21 @@
 
 """Go over all open stabilization bugs for that arch, and drop the arch."""
 
-import json
 import sys
-import urllib.request as urllib
-from typing import TypedDict
-from urllib.parse import urlencode
 
+from pkgcore.bugzilla import (
+    BugQuery,
+    BugUpdate,
+    Component,
+    ListChange,
+    NewComment,
+    Status,
+)
+from pkgcore.bugzilla.apikey import BugzillaClientArgs
 from pkgcore.util import commandline
 
 argparser = commandline.ArgumentParser(version=False, description=__doc__)
-argparser.add_argument(
-    "--api-key",
-    metavar="KEY",
-    required=True,
-    help="Bugzilla API key",
-    docs="""
-        The Bugzilla API key to use for authentication. Used mainly to overcome
-        rate limiting done by bugzilla server. This tool doesn't perform any
-        bug editing, just fetching info for the bug.
-    """,
-)
+BugzillaClientArgs.mangle_argparser(argparser)
 argparser.add_argument(
     "--arch",
     metavar="ARCH",
@@ -35,80 +30,40 @@ argparser.add_argument(
 )
 
 
-class BugInfo(TypedDict):
-    id: int
-    cc: list[str]
-
-
 @argparser.bind_final_check
 def check_args(parser, namespace):
-    repo = namespace.domain.ebuild_repos
+    repo = namespace.domain.ebuild_repos_raw
     namespace.known_arches = frozenset().union(*(pkg.known_arches for pkg in repo))
 
     if namespace.arch not in namespace.known_arches:
         parser.error(f"unknown arch: {namespace.arch}")
 
 
-def fetch_bugs(arch: str, api_key: str) -> tuple[BugInfo, ...]:
-    params = urlencode(
-        (
-            ("Bugzilla_api_key", api_key),
-            ("component", "Stabilization"),
-            ("include_fields", ",".join(BugInfo.__annotations__)),
-            ("bug_status", "UNCONFIRMED"),
-            ("bug_status", "CONFIRMED"),
-            ("bug_status", "IN_PROGRESS"),
-            ("cc", f"{arch}@gentoo.org"),
-        )
-    )
-    with urllib.urlopen(
-        "https://bugs.gentoo.org/rest/bug?" + params, timeout=30
-    ) as response:
-        return tuple(json.loads(response.read().decode("utf-8")).get("bugs", []))
-
-
-def update_bug(arch: str, api_key: str, bug_id: int, to_close: bool):
-    req = {
-        "Bugzilla_api_key": api_key,
-        "ids": [bug_id],
-        "cc": {"remove": [f"{arch}@gentoo.org"]},
-    }
-
+def destable(arch: str, last: bool) -> BugUpdate:
+    """Drop the arch from CC, closing the bug if it was the only one left"""
+    uncc = ListChange.removing(f"{arch}@gentoo.org")
     comment = f"Arch {arch} is destabled, removing."
-    if to_close:
-        req["status"] = "RESOLVED"
-        req["resolution"] = "FIXED"
-        comment += "\n\nNo remaining arches, closing the bug."
-    else:
-        req["status"] = "IN_PROGRESS"
-    req["comment"] = {"body": comment}
-
-    data = json.dumps(req).encode("utf-8")
-    url = f"https://bugs.gentoo.org/rest/bug/{bug_id}"
-    request = urllib.Request(url, data=data, method="PUT")
-    request.add_header("Content-Type", "application/json")
-    with urllib.urlopen(request, timeout=30) as response:
-        return json.loads(response.read().decode("utf-8"))
+    if last:
+        return BugUpdate.resolve(
+            comment=f"{comment}\n\nNo remaining arches, closing the bug.", cc=uncc
+        )
+    return BugUpdate(status=Status.IN_PROGRESS, cc=uncc, comment=NewComment(comment))
 
 
 @argparser.bind_main_func
 def main(options, out, err):
-    for i, bug in enumerate(bugs := fetch_bugs(options.arch, options.api_key), start=1):
-        cc = frozenset(bug["cc"])
-        cc_names = frozenset(
-            x.split("@", 1)[0] for x in cc if x.endswith("@gentoo.org") or "@" not in x
-        )
-        bug_arches = cc_names.intersection(options.known_arches)
-
-        out.write(f"[{i}/{len(bugs)}] https://bugs.gentoo.org/{bug['id']}")
+    query = (
+        BugQuery.component(Component.STABILIZATION)
+        & BugQuery.unresolved()
+        & BugQuery.cc(f"{options.arch}@gentoo.org")
+    )
+    bugs = options.bugzilla.search(query)
+    for i, bug in enumerate(bugs.values(), start=1):
+        out.write(f"[{i}/{len(bugs)}] {bug.url}")
         out.flush()
 
-        update_bug(
-            arch=options.arch,
-            api_key=options.api_key,
-            bug_id=bug["id"],
-            to_close=len(bug_arches) == 1,
-        )
+        remaining = set(bug.arches(options.known_arches)) - {options.arch}
+        options.bugzilla.update(bug.id, destable(options.arch, not remaining))
 
 
 if __name__ == "__main__":

diff --git a/examples/set_maintainers.py b/examples/set_maintainers.py
index ce5317046..b20205f3b 100755
--- a/examples/set_maintainers.py
+++ b/examples/set_maintainers.py
@@ -1,108 +1,60 @@
 #!/usr/bin/env python3
 
-import json
+"""Assign bug-wrangler owned keywording and stabilization bugs to maintainers."""
+
 import sys
-import urllib.request as urllib
-from urllib.parse import urlencode
 
-from pkgcore.ebuild.atom import atom
-from pkgcore.ebuild.errors import MalformedAtom
+from pkgcore.bugzilla import (
+    BugCategory,
+    BugQuery,
+    BugUpdate,
+    BugzillaError,
+    ListChange,
+)
+from pkgcore.bugzilla.apikey import BugzillaClientArgs
+from pkgcore.bugzilla.changes import MAINTAINER_NEEDED
 from pkgcore.util import commandline
 
-argparser = commandline.ArgumentParser(color=False, version=False)
-argparser.add_argument(
-    "--api-key",
-    metavar="KEY",
-    required=True,
-    help="Bugzilla API key",
-    docs="""
-        The Bugzilla API key to use for authentication. Used mainly to overcome
-        rate limiting done by bugzilla server. This tool doesn't perform any
-        bug editing, just fetching info for the bug.
-    """,
+argparser = commandline.ArgumentParser(color=False, version=False, description=__doc__)
+BugzillaClientArgs.mangle_argparser(argparser)
+
+QUERY = (
+    BugQuery.assigned_to("bug-wranglers")
+    & BugQuery.category(BugCategory.STABLEREQ, BugCategory.KEYWORDREQ)
+    & BugQuery.unresolved()
 )
 
 
 @argparser.bind_final_check
 def check_args(parser, namespace):
-    namespace.repo = namespace.domain.ebuild_repos
+    # raw, so packages the profile filters out still yield their maintainers
+    namespace.repo = namespace.domain.ebuild_repos_raw
 
 
-def fetch_bugs():
-    params = urlencode(
-        (
-            ("assigned_to", "bug-wranglers"),
-            ("component", "Stabilization"),
-            ("component", "Keywording"),
-            (
-                "include_fields",
-                "id,cf_stabilisation_atoms",
-            ),
-            ("bug_status", "UNCONFIRMED"),
-            ("bug_status", "CONFIRMED"),
-            ("bug_status", "IN_PROGRESS"),
-        )
-    )
-    with urllib.urlopen(
-        "https://bugs.gentoo.org/rest/bug?" + params, timeout=30
-    ) as response:
-        reply = json.loads(response.read().decode("utf-8")).get("bugs", [])
-    return {
-        bug["id"]: bug["cf_stabilisation_atoms"].splitlines()
-        for bug in reply
-        if bug["cf_stabilisation_atoms"].strip()
-    }
-
-
-def parse_atom(pkg: str):
-    try:
-        return atom(pkg)
-    except MalformedAtom as exc:
-        try:
-            return atom(f"={pkg}")
-        except MalformedAtom:
-            raise exc
-
-
-def collect_maintainers(repo, atoms):
-    for a in atoms:
-        for pkg in repo.itermatch(parse_atom(a.split(" ", 1)[0]).unversioned_atom):
+def collect_maintainers(repo, bug):
+    for a in bug.package_list.atoms:
+        for pkg in repo.itermatch(a.unversioned_atom):
             for maintainer in pkg.maintainers:
                 yield maintainer.email
 
 
 @argparser.bind_main_func
 def main(options, out, err):
-    for bug_id, atoms in fetch_bugs().items():
+    for bug in options.bugzilla.search(QUERY).values():
+        if not bug.package_list:
+            continue
         try:
-            maintainers = dict.fromkeys(collect_maintainers(options.repo, atoms)) or (
-                "[email protected]",
+            maintainers = dict.fromkeys(collect_maintainers(options.repo, bug)) or (
+                MAINTAINER_NEEDED,
             )
             assignee, *add_cc = maintainers
-
-            request_data = dict(
-                Bugzilla_api_key=options.api_key,
-                cc_add=add_cc,
-                assigned_to=assignee,
-            )
-            request = urllib.Request(
-                url=f"https://bugs.gentoo.org/rest/bug/{bug_id}",
-                data=json.dumps(request_data).encode("utf-8"),
-                method="PUT",
-                headers={
-                    "Content-Type": "application/json",
-                    "Accept": "application/json",
-                },
-            )
-            with urllib.urlopen(request, timeout=30) as response:
-                reply = response.read().decode("utf-8")
-            out.write(f"Bug: {bug_id}, replied: {reply}")
-        except MalformedAtom:
-            err.write(
-                err.fg("red"),
-                f"Malformed bug {bug_id} with atoms: {', '.join(atoms)}",
-                err.reset,
+            changes = options.bugzilla.update(
+                bug.id,
+                BugUpdate(assigned_to=assignee, cc=ListChange.adding(*add_cc)),
             )
+            out.write(f"Bug: {bug.id}, assigned to {assignee}, changed: {changes!r}")
+        except BugzillaError as exc:
+            err.write(err.fg("red"), f"Bug {bug.id}: {exc}", err.reset)
 
 
 if __name__ == "__main__":

diff --git a/examples/verify_at_done.py b/examples/verify_at_done.py
index 125933731..e8df79500 100755
--- a/examples/verify_at_done.py
+++ b/examples/verify_at_done.py
@@ -2,118 +2,98 @@
 
 """Go over all open stabilization or keywording bugs, and check for done bugs."""
 
-import json
 import sys
-import urllib.request as urllib
-from typing import TypedDict
-from urllib.parse import urlencode
 
-from pkgcore.ebuild.atom import atom
-from pkgcore.ebuild.errors import MalformedAtom
+from pkgcore.bugzilla import (
+    BugCategory,
+    BugQuery,
+    BugzillaError,
+    Component,
+    FlagStatus,
+)
+from pkgcore.bugzilla.apikey import BugzillaClientArgs
+from pkgcore.bugzilla.pkglist import ALL_KEYWORDS, NO_KEYWORDS, SAME_KEYWORDS
 from pkgcore.util import commandline
 
 argparser = commandline.ArgumentParser(version=False, description=__doc__)
-argparser.add_argument(
-    "--api-key",
-    metavar="KEY",
-    required=True,
-    help="Bugzilla API key",
-    docs="""
-        The Bugzilla API key to use for authentication. Used mainly to overcome
-        rate limiting done by bugzilla server. This tool doesn't perform any
-        bug editing, just fetching info for the bug.
-    """,
-)
+BugzillaClientArgs.mangle_argparser(argparser)
 
+QUERY = (
+    BugQuery.component(Component.STABILIZATION, Component.KEYWORDING)
+    & BugQuery.unresolved()
+    & BugQuery.flag("sanity-check", FlagStatus.GRANTED)
+)
 
-class BugInfo(TypedDict):
-    id: int
-    cf_stabilisation_atoms: str
-    component: str
-    cc: list[str]
+UNEXPANDED = frozenset((ALL_KEYWORDS, SAME_KEYWORDS))
 
 
 @argparser.bind_final_check
 def check_args(parser, namespace):
-    namespace.repo = namespace.domain.ebuild_repos
-
-
-def fetch_bugs(api_key: str) -> tuple[BugInfo, ...]:
-    params = urlencode(
-        (
-            ("Bugzilla_api_key", api_key),
-            ("component", "Stabilization"),
-            ("component", "Keywording"),
-            ("include_fields", ",".join(BugInfo.__annotations__)),
-            ("bug_status", "UNCONFIRMED"),
-            ("bug_status", "CONFIRMED"),
-            ("bug_status", "IN_PROGRESS"),
-            ("f1", "flagtypes.name"),
-            ("o1", "anywords"),
-            ("v1", "sanity-check+"),
-        )
+    namespace.repo = namespace.domain.ebuild_repos_raw
+    namespace.known_arches = frozenset().union(
+        *(repo.known_arches for repo in namespace.repo)
     )
-    with urllib.urlopen(
-        "https://bugs.gentoo.org/rest/bug?" + params, timeout=30
-    ) as response:
-        return tuple(json.loads(response.read().decode("utf-8")).get("bugs", []))
 
 
-def parse_atom(pkg: str):
-    try:
-        return atom(pkg)
-    except MalformedAtom as exc:
-        try:
-            return atom(f"={pkg}")
-        except MalformedAtom:
-            raise exc
+def requested_arches(entry, cc_arches):
+    """The arches a package list line asks for.
 
+    A line carrying no keywords of its own inherits the whole CC list, which is
+    how nattka reads it too.
+    """
+    keywords = frozenset(x.lstrip("~") for x in entry.keywords)
+    if NO_KEYWORDS in keywords:
+        return frozenset()
+    return keywords or frozenset(cc_arches)
 
-def collect_packages(repo, bug: BugInfo):
-    return tuple(
-        pkg
-        for a in bug["cf_stabilisation_atoms"].splitlines()
-        if (b := " ".join(a.split()))
-        for pkg in repo.itermatch(parse_atom(b.split(" ", 1)[0]))
-    )
+
+def pending_packages(repo, bug, cc_arches):
+    """Map each requested arch to the packages it still has to stabilize.
+
+    Returns None when the bug can't be judged, either because an atom matches
+    nothing in the repo or because the package list still holds unexpanded
+    keyword shorthands. Concluding from a partial view is how an arch gets told
+    it is done while a package the repo never matched is still waiting on it.
+    """
+    pending: dict[str, list] = {}
+    for entry in bug.package_list.entries:
+        if entry.pkg is None:
+            continue
+        if UNEXPANDED & frozenset(entry.keywords):
+            return None
+        if not (pkgs := tuple(repo.itermatch(entry.pkg))):
+            return None
+        for arch in requested_arches(entry, cc_arches):
+            pending.setdefault(arch, []).extend(pkgs)
+    return pending
 
 
 @argparser.bind_main_func
 def main(options, out, err):
-    for bug in fetch_bugs(options.api_key):
+    for bug in options.bugzilla.search(QUERY).values():
+        # the heuristic for keywording is wrong, skip those for now
+        if bug.category is BugCategory.KEYWORDREQ:
+            continue
+        cc_arches = bug.arches(options.known_arches)
         try:
-            pkgs = collect_packages(options.repo, bug)
-            if not pkgs:
+            pending = pending_packages(options.repo, bug, cc_arches)
+        except BugzillaError as exc:
+            err.write(err.fg("red"), f">>> {exc}", err.reset)
+            continue
+        if not pending:
+            continue
+
+        for arch in cc_arches:
+            if not (pkgs := pending.get(arch)):
                 continue
-            for cc in bug["cc"]:
-                cc = cc.removesuffix("@gentoo.org")
-                if bug["component"] == "Keywording":
-                    continue  # skip keywording for now, the heuristic is wrong
-                if all(cc in pkg.keywords for pkg in pkgs):
-                    out.write(
-                        out.fg("yellow"),
-                        f"https://bugs.gentoo.org/{bug['id']}, cc: {cc}, all packages are done",
-                        out.reset,
-                        " -> ",
-                        f"nattka resolve -a {cc} {bug['id']}",
-                    )
-                if bug["component"] == "Keywording" and all(
-                    f"~{cc}" in pkg.keywords for pkg in pkgs
-                ):
-                    out.write(
-                        out.fg("yellow"),
-                        f"https://bugs.gentoo.org/{bug['id']}, cc: ~{cc}, all packages are done",
-                        out.reset,
-                        " -> ",
-                        f"nattka resolve -a {cc} {bug['id']}",
-                    )
-        except MalformedAtom as exc:
-            err.write(
-                err.fg("red"),
-                f">>> Malformed bug {bug['id']} with atoms: {', '.join(bug['cf_stabilisation_atoms'].splitlines())}",
-                err.reset,
-                str(exc),
-            )
+            if all(arch in pkg.keywords for pkg in pkgs):
+                out.write(
+                    out.fg("yellow"),
+                    f"{bug.url}, cc: {arch}, all packages are done",
+                    out.reset,
+                    " -> ",
+                    f"nattka resolve -a {arch} {bug.id}",
+                )
 
 
 if __name__ == "__main__":
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.