From ffee4c9f3c2865ed6ff3cfd665a0a1b10623de7a Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 19 Nov 2024 10:46:18 +0100 Subject: [PATCH 01/16] rpm_dep_tree: graph of (Build)Requires for a rpm --- scripts/rpmwatcher/repoquery.py | 153 ++++++++++++++++++++++++++++++++ scripts/rpmwatcher/rpm_dep_tree | 100 +++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 scripts/rpmwatcher/repoquery.py create mode 100755 scripts/rpmwatcher/rpm_dep_tree diff --git a/scripts/rpmwatcher/repoquery.py b/scripts/rpmwatcher/repoquery.py new file mode 100644 index 00000000..46264b25 --- /dev/null +++ b/scripts/rpmwatcher/repoquery.py @@ -0,0 +1,153 @@ +import logging +import os +import re +import subprocess +from typing import Iterable, Sequence + +XCPNG_YUMREPO_TMPL = """ +[xcpng-{section}{suffix}] +name=xcpng - {section}{suffix} +baseurl=https://updates.xcp-ng.org/8/{version}/{section}/{rpmarch}/ +gpgkey=https://xcp-ng.org/RPM-GPG-KEY-xcpng +failovermethod=priority +skip_if_unavailable=False +""" + +# DNF v4 adds an implicit trailing newline to --qf format, but v5 does not +dnf_version = subprocess.check_output(['dnf', '--version'], universal_newlines=True).strip().split('.') +if int(dnf_version[0]) >= 5: + QFNL = "\n" +else: + QFNL = "" + +def setup_xcpng_yum_repos(*, yum_repo_d: str, sections: Iterable[str], + bin_arch: str | None, version: str) -> None: + with open(os.path.join(yum_repo_d, "xcpng.repo"), "w") as yumrepoconf: + for section in sections: + # binaries + block = XCPNG_YUMREPO_TMPL.format(rpmarch=bin_arch, + section=section, + version=version, + suffix='', + ) + yumrepoconf.write(block) + # sources + block = XCPNG_YUMREPO_TMPL.format(rpmarch='Source', + section=section, + version=version, + suffix='-src', + ) + yumrepoconf.write(block) + +DNF_BASE_CMD = None +def dnf_setup(*, dnf_conf: str, yum_repo_d: str) -> None: + global DNF_BASE_CMD + DNF_BASE_CMD = ['dnf', '--quiet', + '--releasever', 'WTF', + '--config', dnf_conf, + f'--setopt=reposdir={yum_repo_d}', + ] + +BINRPM_SOURCE_CACHE: dict[str, str] = {} +def rpm_source_package(rpmname: str) -> str: + return BINRPM_SOURCE_CACHE[rpmname] + +def run_repoquery(args: list[str], split: bool = True) -> str | Sequence[str]: + assert DNF_BASE_CMD is not None + cmd = DNF_BASE_CMD + ['repoquery'] + args + logging.debug('$ %s', ' '.join(cmd)) + output = subprocess.check_output(cmd, universal_newlines=True).strip() + logging.debug('> %s', output) + return output.split() if split else output + +SRPM_BINRPMS_CACHE: dict[str, set[str]] = {} # binrpm-nevr -> srpm-nevr +def fill_srpm_binrpms_cache() -> None: + # HACK: get nevr for what dnf outputs as %{sourcerpm} + logging.debug("get epoch info for SRPMs") + args = [ + '--disablerepo=*', '--enablerepo=*-src', '*', + '--qf', '%{name}-%{version}-%{release}.src.rpm,%{name}-%{evr}' + QFNL, + '--latest-limit=1', + ] + SRPM_NEVR_CACHE = { # sourcerpm -> srpm-nevr + sourcerpm: nevr + for sourcerpm, nevr in (line.split(',') + for line in run_repoquery(args)) + } + + # binary -> source mapping + logging.debug("get binary to source mapping") + global SRPM_BINRPMS_CACHE, BINRPM_SOURCE_CACHE + args = [ + '--disablerepo=*-src', '*', + '--qf', '%{name}-%{evr},%{sourcerpm}' + QFNL, # FIXME no epoch in sourcerpm, why does it work? + '--latest-limit=1', + ] + BINRPM_SOURCE_CACHE = { + # packages without source are not in SRPM_NEVR_CACHE, fallback to sourcerpm + binrpm: SRPM_NEVR_CACHE.get(sourcerpm, srpm_strip_src_rpm(sourcerpm)) + for binrpm, sourcerpm in (line.split(',') + for line in run_repoquery(args)) + } + + # reverse mapping source -> binaries + SRPM_BINRPMS_CACHE = {} + for binrpm, srpm in BINRPM_SOURCE_CACHE.items(): + binrpms = SRPM_BINRPMS_CACHE.get(srpm, set()) + if not binrpms: + SRPM_BINRPMS_CACHE[srpm] = binrpms + binrpms.add(binrpm) + +def srpm_nevr(rpmname: str) -> str: + args = [ + '--disablerepo=*', '--enablerepo=*-src', + '--qf=%{name}-%{evr}' + QFNL, # to get the epoch only when non-zero + '--latest-limit=1', + rpmname, + ] + ret = run_repoquery(args) + assert ret, f"Found no SRPM named {rpmname}" + assert len(ret) == 1 # ensured by --latest-limit=1 ? + return ret[0] + +# dnf insists on spitting .src.rpm names it cannot take as input itself +def srpm_strip_src_rpm(srpmname: str) -> str: + SUFFIX = ".src.rpm" + assert srpmname.endswith(SUFFIX), f"{srpmname} does not end in .src.rpm" + nrv = srpmname[:-len(SUFFIX)] + return nrv + +def rpm_requires(rpmname: str) -> Sequence[str]: + args = [ + '--disablerepo=*-src', # else requires of same-name SRPM are included + '--qf=%{name}-%{evr}' + QFNL, # to avoid getting the arch and explicit zero epoch + '--resolve', + '--requires', rpmname, + ] + ret = run_repoquery(args) + return ret + +def srpm_requires(srpmname: str) -> set[str]: + args = [ + '--qf=%{name}-%{evr}' + QFNL, # to avoid getting the arch + '--resolve', + '--requires', f"{srpmname}.src", + ] + ret = set(run_repoquery(args)) + return ret + +def srpm_binrpms(srpmname: str) -> set[str]: + ret = SRPM_BINRPMS_CACHE.get(srpmname, None) + if ret is None: # FIXME should not happen + logging.error("%r not found in cache", srpmname) + assert False + return [] + logging.debug("binrpms for %s: %s", srpmname, ret) + return ret + +UPSTREAM_REGEX = re.compile(r'\.el[0-9]+(_[0-9]+)?(\..*|)$') +RPM_NVR_SPLIT_REGEX = re.compile(r'^(.+)-([^-]+)-([^-]+)$') +def is_pristine_upstream(rpmname:str) -> bool: + if re.search(UPSTREAM_REGEX, rpmname): + return True + return False diff --git a/scripts/rpmwatcher/rpm_dep_tree b/scripts/rpmwatcher/rpm_dep_tree new file mode 100755 index 00000000..c0e3cf43 --- /dev/null +++ b/scripts/rpmwatcher/rpm_dep_tree @@ -0,0 +1,100 @@ +#! /usr/bin/env python3 + +import atexit +import logging +import re +import sys +import tempfile + +import repoquery + +ARCH = "x86_64" +SHOW_BOUNDARY = False + +# Tell if package is pristine upstream, or part of well-kown list of +# packages we want to consider as "upstream" rather than forks +def is_upstream(rpmname: str) -> bool: + if repoquery.is_pristine_upstream(rpmname): + return True + m = re.match(repoquery.RPM_NVR_SPLIT_REGEX, rpmname) + assert m, f"{rpmname!r} does not match {repoquery.RPM_NVR_SPLIT_REGEX!r}" + if m.group(1) in ['systemd', 'util-linux', 'ncurses', + #'xapi', + 'devtoolset-11-gcc', 'devtoolset-11-binutils']: + return True + return False + +def main() -> int: + logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.DEBUG) + + this_exe, version, root_srpm = sys.argv + + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod, + open(f"{root_srpm}-{version}.dot", "w") as dotfile): + + repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'updates'], + bin_arch=ARCH, + version=version) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + + repoquery.fill_srpm_binrpms_cache() + # print([x for x in sorted(SRPM_BINRPMS_CACHE.keys()) if x.startswith("openssl-")]) + # return 0 + + print("digraph packages {", file=dotfile) + srpms_seen: set[str] = set() + new_srpms = {repoquery.srpm_nevr(root_srpm)} + while new_srpms: + next_srpms = set() # preparing next round's new_srpms + logging.info("seen: %s, new: %s", len(srpms_seen), len(new_srpms)) + logging.debug(" new: %s", new_srpms) + for srpm in new_srpms: + # draw source packages themselves + if is_upstream(srpm): + if SHOW_BOUNDARY: + print(f'"{srpm}" [color=grey];', file=dotfile) + logging.debug("skipping upstream %s", srpm) + continue # we don't rebuild upstream rpms + elif ".xcpng8.3.": + print(f'"{srpm}";', file=dotfile) + else: + print(f'"{srpm}" [color=red];', file=dotfile) + + # build reqs + breqs = {repoquery.rpm_source_package(breq) + for breq in repoquery.srpm_requires(srpm)} + logging.debug("%s req sources: %s", len(breqs), breqs) + + # reqs of binary rpms produced + reqs = set() + for binrpm in repoquery.srpm_binrpms(srpm): + reqs.update({repoquery.rpm_source_package(req) + for req in repoquery.rpm_requires(binrpm)}) + + # draw breqs, plain + for breq in breqs: + if (not SHOW_BOUNDARY) and is_upstream(breq): + continue + print(f'"{srpm}" -> "{breq}";', file=dotfile) + # draw additional runtime reqs, dotted + for req in reqs.difference(breqs): + if (not SHOW_BOUNDARY) and is_upstream(req): + continue + if srpm == req: + continue # dependency between RPMs of this SRPM + print(f'"{srpm}" -> "{req}" [style=dotted];', file=dotfile) + + # accumulate + srpms_seen.update(new_srpms) + next_srpms.update(breqs.difference(srpms_seen)) + next_srpms.update(reqs.difference(srpms_seen)) + + new_srpms = next_srpms + + print("}", file=dotfile) + return 0 + +if __name__ == "__main__": + sys.exit(main()) From 4e8fd5da5c2438560ee167d5251f9e804944b138 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 10 Feb 2025 17:38:46 +0100 Subject: [PATCH 02/16] yum_repo_query: repoquery for xs8 and xcpng8.3 repos --- scripts/rpmwatcher/yum_repo_query | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 scripts/rpmwatcher/yum_repo_query diff --git a/scripts/rpmwatcher/yum_repo_query b/scripts/rpmwatcher/yum_repo_query new file mode 100755 index 00000000..f0e5803a --- /dev/null +++ b/scripts/rpmwatcher/yum_repo_query @@ -0,0 +1,35 @@ +#! /usr/bin/env python3 + +import logging +import sys +import tempfile + +import repoquery + +ARCH = "x86_64" +XCP_VERSION = "8.3" + +# Use `repoquery` on xs8 and xcpng8.3 repos +def main() -> int: + logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO) + + args = sys.argv[1:] + + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + + repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'updates'], + bin_arch=ARCH, + version=XCP_VERSION) + repoquery.setup_xs8_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'normal', 'earlyaccess'], + ) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + + print(repoquery.run_repoquery(args, split=False)) + + return 0 + +if __name__ == "__main__": + sys.exit(main()) From 937197e038b9bd9bdba4e66ff16a032cdbe7fe32 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 7 Feb 2025 17:30:22 +0100 Subject: [PATCH 03/16] WIP yum_repo_status: generate a table showing how much outdated 8.3 is from xs8 FIXME: - include successive xs8 update waves - migrate infos from Sam's wiki page to package_status.csv - show binrpms? - include "current" el7 versions - link proprietary packages replaced by ours - should take import_reason from provenance.csv as tooltip? - include upstream releases from GH? --- scripts/rpmwatcher/package_status.csv | 60 ++++++ scripts/rpmwatcher/repoquery.py | 92 +++++++-- scripts/rpmwatcher/style.css | 57 ++++++ scripts/rpmwatcher/yum_repo_status | 259 ++++++++++++++++++++++++++ 4 files changed, 457 insertions(+), 11 deletions(-) create mode 100644 scripts/rpmwatcher/package_status.csv create mode 100644 scripts/rpmwatcher/style.css create mode 100755 scripts/rpmwatcher/yum_repo_status diff --git a/scripts/rpmwatcher/package_status.csv b/scripts/rpmwatcher/package_status.csv new file mode 100644 index 00000000..5782bb3a --- /dev/null +++ b/scripts/rpmwatcher/package_status.csv @@ -0,0 +1,60 @@ +SRPM_name;status;comment +auto-cert-kit;ignored;unused, why? +automake16;ignored;unused, why? +bpftool;ignored;unused, why? +capstone;ignored;unused, why? +citrix-crypto-module;ignored;proprietary, patched FIPS openssl +compiler-rt18;ignored;unused, why? +dlm;ignored;previous dependency for corosync +emu-manager;ignored;proprietary, replaced by xcp-emu-manager +epel-release;ignored;unused, why? +forkexecd;ignored;now in xapi +fuse;;breq for e2fsprogs 1.47 +gfs2-utils;ignored;unsupported fs +glib2;ignored;same version as el7, why? +golang;ignored;for newer xe-guest-utilities +hcp_nss;ignored;unused, “enforce any permitted user login as root” +hwloc;ignored;unused, why? +libbpf;ignored;unused, why? +libcgroup;ignored;unused, same version as el7, why? +libhbalinux;;unused? el7 fork, “Fix crash in fcoeadm/elxhbamgr on certain machines” +libnbd;ignored;unused, dep for xapi-storage-plugins +linuxconsoletools;ignored;unused, same version as el7, why? +mbootpack;;for secureboot? +message-switch;ignored;now in xapi +mpdecimal;ignored;unused, why? +ninja-build;ignored;unused +pbis-open;ignored;unused, likely linked to upgrade-pbis-to-winbind +pbis-open-upgrade;ignored;unused, likely linked to upgrade-pbis-to-winbind +pvsproxy;ignored;proprietary +python-monotonic;ignored;previous dependency for sm +python-tqdm;ignored;unused, dependency for pvsproxy +rrdd-plugins;ignored;now in xapi +ruby;ignored;unused, why? +sbd;;unused? "storage-based death functionality" +sm-cli;ignored;now in xapi +secureboot-certificates;ignored;proprietary, needs alternative? +security-tools;ignored;proprietary, pool_secret tool +sm-transport-lib;ignored;proprietary +squeezed;ignored;now in xapi +tix;ignored;unused, why? +upgrade-pbis-to-winbind;ignored;proprietary +v6d;ignored;proprietary, replaced by xcp-featured +varstored-guard;ignored;now in xapi +vendor-update-keys;ignored;proprietary +vgpu;ignored;proprietary +vhd-tool;ignored;now in xapi +wsproxy;ignored;now in xapi +xapi-clusterd;ignored;proprietary +xapi-nbd;ignored;now in xapi +xapi-storage;ignored;now in xapi +xapi-storage-plugins;ignored;proprietarized, forked as xcp-ng-xapi-storage +xapi-storage-script;ignored;now in xapi +xcp-networkd;ignored;now in xapi +xcp-rrdd;ignored;now in xapi +xencert;;"automated testkit for certifying storage hardware with XenServer" +xenopsd;ignored;now in xapi +xenserver-release;forked;xcp-ng-release +xenserver-snmp-agent;ignored;proprietary, SNMP MIB +xenserver-telemetry;ignored;proprietary, xapi plugin +xs-clipboardd;ignored;proprietary, replaced by xcp-clipboardd diff --git a/scripts/rpmwatcher/repoquery.py b/scripts/rpmwatcher/repoquery.py index 46264b25..51e6ce43 100644 --- a/scripts/rpmwatcher/repoquery.py +++ b/scripts/rpmwatcher/repoquery.py @@ -13,6 +13,15 @@ skip_if_unavailable=False """ +XCPNG_YUMREPO_USER_TMPL = """ +[xcpng-{section}{suffix}] +name=xcpng - {section}{suffix} +baseurl=https://koji.xcp-ng.org/repos/user/8/{version}/{section}/{rpmarch}/ +gpgkey=https://xcp-ng.org/RPM-GPG-KEY-xcpng +failovermethod=priority +skip_if_unavailable=False +""" + # DNF v4 adds an implicit trailing newline to --qf format, but v5 does not dnf_version = subprocess.check_output(['dnf', '--version'], universal_newlines=True).strip().split('.') if int(dnf_version[0]) >= 5: @@ -24,19 +33,47 @@ def setup_xcpng_yum_repos(*, yum_repo_d: str, sections: Iterable[str], bin_arch: str | None, version: str) -> None: with open(os.path.join(yum_repo_d, "xcpng.repo"), "w") as yumrepoconf: for section in sections: + # HACK: use USER_TMPL if section ends with a number + if section[-1].isdigit(): + tmpl = XCPNG_YUMREPO_USER_TMPL + else: + tmpl = XCPNG_YUMREPO_TMPL + # binaries - block = XCPNG_YUMREPO_TMPL.format(rpmarch=bin_arch, - section=section, - version=version, - suffix='', - ) - yumrepoconf.write(block) + if bin_arch: + block = tmpl.format(rpmarch=bin_arch, + section=section, + version=version, + suffix='', + ) + yumrepoconf.write(block) # sources - block = XCPNG_YUMREPO_TMPL.format(rpmarch='Source', - section=section, - version=version, - suffix='-src', - ) + block = tmpl.format(rpmarch='Source', + section=section, + version=version, + suffix='-src', + ) + yumrepoconf.write(block) + + +XS8_YUMREPO_TMPL = """ +[xs8-{section}] +name=XS8 - {section} +baseurl=http://10.1.0.94/repos/XS8/{section}/xs8p-{section}/ +failovermethod=priority +skip_if_unavailable=False + +[xs8-{section}-src] +name=XS8 - {section} source +baseurl=http://10.1.0.94/repos/XS8/{section}/xs8p-{section}-source/ +failovermethod=priority +skip_if_unavailable=False +""" + +def setup_xs8_yum_repos(*, yum_repo_d: str, sections: Iterable[str])-> None: + with open(os.path.join(yum_repo_d, "xs8.repo"), "w") as yumrepoconf: + for section in sections: + block = XS8_YUMREPO_TMPL.format(section=section) yumrepoconf.write(block) DNF_BASE_CMD = None @@ -151,3 +188,36 @@ def is_pristine_upstream(rpmname:str) -> bool: if re.search(UPSTREAM_REGEX, rpmname): return True return False + +def rpm_parse_nevr(nevr: str, suffix: str) -> tuple[str, str, str, str]: + "Parse into (name, epoch:version, release) stripping suffix from release" + m = re.match(RPM_NVR_SPLIT_REGEX, nevr) + assert m, f"{nevr} does not match NEVR pattern" + n, ev, r = m.groups() + if ":" in ev: + e, v = ev.split(":") + else: + e, v = "0", ev + if r.endswith(suffix): + r = r[:-len(suffix)] + return (n, e, v, r) + +def all_binrpms() -> set[str]: + args = [ + '--disablerepo=*-src', + '--qf=%{name}-%{evr}' + QFNL, # to avoid getting the arch + '--latest-limit=1', # only most recent for each package + '*', + ] + ret = set(run_repoquery(args)) + return ret + +def all_srpms() -> set[str]: + args = [ + '--disablerepo=*', '--enablerepo=*-src', + '--qf=%{name}-%{evr}' + QFNL, # to avoid getting the arch + '--latest-limit=1', # only most recent for each package + '*', + ] + ret = set(run_repoquery(args)) + return ret diff --git a/scripts/rpmwatcher/style.css b/scripts/rpmwatcher/style.css new file mode 100644 index 00000000..aeca09f2 --- /dev/null +++ b/scripts/rpmwatcher/style.css @@ -0,0 +1,57 @@ +table { + border-collapse: collapse; + border-spacing: 0; +} +td { + padding: 2px 5px; + /* background-color: lightblue; */ +} + +tr.header { + vertical-align: top; +} +tr.ignored { + color: #888888; + background-color: #cccccc; +} +tr.notused { /* still to check */ + color: red; + background-color: #cccccc; +} + +th.xs { + background-color: #ddddff; +} +th.xcp { + background-color: #ddffdd; +} + +td.pkgname { + white-space: nowrap; +} + +.nosource { + color: red; + background-color: black; +} + +.unexpected { + color: red; +} + +.outdated { + background-color: red; +} + +.uptodate { + background-color: lightgreen; +} + +.better { + background-color: #77cc00; +} + +.upstream { + /* must not be background-color, can exist in outdated/uptodate/better */ + color: blue; +} diff --git a/scripts/rpmwatcher/yum_repo_status b/scripts/rpmwatcher/yum_repo_status new file mode 100755 index 00000000..843830d9 --- /dev/null +++ b/scripts/rpmwatcher/yum_repo_status @@ -0,0 +1,259 @@ +#! /usr/bin/env python3 + +import argparse +from collections import namedtuple, OrderedDict +import csv +import logging +import os +import rpm # type: ignore +import sys +import tempfile +from typing import Iterable, Iterator, Literal, Sequence + +import repoquery + +ARCH = "x86_64" +XCP_VERSION = "8.3" +FILTER_UPSTREAM = False + +def evr_format(evr: tuple[str, str, str] | None | Literal['=']) -> str: + if evr == "=": # dirty special case + return evr + if evr is None: + return "-" + return (f"{evr[0]}:" if evr[0] != "0" else "") + "-".join(evr[1:]) + +# Filters an iterator of (n, e, v, r) for newest evr of each `n`. +# Older versions are allowed to appear before the newer ones. +def filter_best_evr(nevrs: Iterable[tuple[str, str, str, str]] + ) -> Iterator[tuple[str, str, str, str]]: + best: dict[str, tuple[str, str, str]] = {} + for (n, e, v, r) in nevrs: + if n not in best or rpm.labelCompare(best[n], (e, v, r)) < 0: + best[n] = (e, v, r) + yield (n, e, v, r) + # else (e, v, r) is older than a previously-seen version, drop + +def collect_data_xcpng() -> OrderedDict: + xcp_sets = OrderedDict() + for (label, sections) in (("released", ['base', 'updates']), + ("candidates", ["candidates"]), + ("testing", ["testing"]), + ("ci", ["ci"]), + ("incoming", ["incoming"]), + #("ydi1", ["ydi1"]), + ("dtt1", ["dtt1"]), + ): + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + + repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, + sections=sections, + bin_arch=None, + version=XCP_VERSION) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + #repoquery.fill_srpm_binrpms_cache() + + logging.debug("get all XCP-ng %s SRPMs", label) + xcp_srpms = {nevr for nevr in repoquery.all_srpms() + if not FILTER_UPSTREAM or not repoquery.is_pristine_upstream(nevr)} + + xcp_sets[label] = { + n: (e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xcpng{XCP_VERSION}") + for nevr in xcp_srpms)} + + logging.info(f"{label}: {len(xcp_sets[label])}") + + return xcp_sets + +def collect_data_xs8(): + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + + repoquery.setup_xs8_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'normal'], + ) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + logging.debug("fill cache with XS info") + repoquery.fill_srpm_binrpms_cache() + + logging.debug("get all XS SRPMs") + xs8_srpms = {nevr for nevr in repoquery.all_srpms() + if not FILTER_UPSTREAM or not repoquery.is_pristine_upstream(nevr)} + xs8_rpms_sources = {nevr for nevr in repoquery.SRPM_BINRPMS_CACHE.keys() + if not FILTER_UPSTREAM or not repoquery.is_pristine_upstream(nevr)} + + xs8_srpms_set = {n: (e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xs8") + for nevr in xs8_srpms)} + xs8_rpms_sources_set = {n: (e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xs8") + for nevr in xs8_rpms_sources)} + + logging.info(f"xs8 src: {len(xs8_srpms_set)}") + logging.info(f"xs8 bin: {len(xs8_rpms_sources_set)}") + + return (xs8_srpms_set, xs8_rpms_sources_set) + +def collect_data_xs8ea(): + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + + repoquery.setup_xs8_yum_repos(yum_repo_d=yumrepod, + sections=['earlyaccess'], + ) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + logging.debug("fill cache with XS EA info") + repoquery.fill_srpm_binrpms_cache() + + logging.debug("get all XS EA SRPMs") + xs8ea_srpms = {nevr for nevr in repoquery.all_srpms() + if not FILTER_UPSTREAM or not repoquery.is_pristine_upstream(nevr)} + + xs8ea_srpms_set = {n: (e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xs8") + for nevr in xs8ea_srpms)} + + logging.info(f"xs8 EA src: {len(xs8ea_srpms_set)}") + + return xs8ea_srpms_set + +def read_package_status_metadata(): + with open('package_status.csv', newline='') as csvfile: + csvreader = csv.reader(csvfile, delimiter=';', quotechar='|') + headers = next(csvreader) + assert headers == ["SRPM_name", "status", "comment"], f"unexpected headers {headers!r}" + PackageStatus = namedtuple("PackageStatus", headers[1:]) # type: ignore[misc] + return {row[0]: PackageStatus(*row[1:]) + for row in csvreader} + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('-v', '--verbose', action='count', default=0) + args = parser.parse_args() + if args.verbose > 2: + args.verbose = 2 + + loglevel = {0: logging.WARNING, + 1: logging.INFO, + 2: logging.DEBUG, + }[args.verbose] + logging.basicConfig(format='[%(levelname)s] %(message)s', level=loglevel) + + PACKAGE_STATUS = read_package_status_metadata() + + # collect data from repos + # ... xcp-ng + xcp_sets = collect_data_xcpng() + # ... xs8 + (xs8_srpms_set, xs8_rpms_sources_set) = collect_data_xs8() + # ... xs8 earlyaccess + xs8ea_srpms_set = collect_data_xs8ea() + + # ... collect + + all_srpms = set(xs8_srpms_set.keys()) | xs8_rpms_sources_set.keys() | xs8ea_srpms_set.keys() + for label, srpms in xcp_sets.items(): + all_srpms |= srpms.keys() + + logging.info(f"all: {len(all_srpms)}") + + # output + + with open(f"repo-status-{XCP_VERSION}.html", "w") as outfile: + print('', file=outfile) + print(f''' + + + XCP-ng {XCP_VERSION} RPM flow + + + + + + + ''', file=outfile) + print('', file=outfile) + + for srpm in sorted(all_srpms): + xs_ver_src = xs8_srpms_set.get(srpm, None) + xsea_ver_src: tuple[str, str, str] | None | Literal["="] + xsea_ver_src = xs8ea_srpms_set.get(srpm, None) + if xs_ver_src and xsea_ver_src == xs_ver_src: + xsea_ver_src = "=" # don't overload with useless info # FIXME + xs_ver_bin = xs8_rpms_sources_set.get(srpm, None) + xcp_vers = {label: xcp_sets[label].get(srpm, None) + for label in xcp_sets} + + srpm_status = PACKAGE_STATUS.get(srpm, None) + + if srpm_status: + tooltip = srpm_status.comment + else: + tooltip = "" + + if srpm_status and srpm_status.status == "ignored": + row_classes = "ignored" + elif all(ver is None for ver in xcp_vers.values()) and xs_ver_bin is None: + row_classes = "notused" + else: + row_classes = "" + + # XS source + xss_classes = "xs" + if srpm in xs8_rpms_sources_set and srpm not in xs8_srpms_set: + xss_classes += " nosource" + + # XS binary + xsb_classes = "xs" + if xs_ver_bin and xs_ver_src != xs_ver_bin: + xsb_classes += " nosource" + + print(f'', file=outfile) + print('
package + XenServer 8 + XCP-ng {XCP_VERSION} +
srcrpmbinrpmEA srcrpm', file=outfile) + for label in xcp_sets: + print(f'{label}', file=outfile) + print(f'
{srpm}' + f'{evr_format(xs_ver_src)}' + f'{evr_format(xs_ver_bin)}', + f'{evr_format(xsea_ver_src)}', + file=outfile) + + # XCP-ng + ref_xs_ver = xs_ver_bin if xs_ver_bin else xs_ver_src + for label in xcp_sets: + xcp_ver = xcp_vers[label] + xcp_ver_str = evr_format(xcp_ver) + classes = "xcp" + if repoquery.is_pristine_upstream(xcp_ver_str): + classes += " upstream" + if not xcp_ver: + pass + elif xcp_ver and xcp_ver == ref_xs_ver: + classes += " uptodate" + elif not ref_xs_ver: + classes += " better" + elif not ref_xs_ver: + pass + elif rpm.labelCompare(xcp_ver, ref_xs_ver) < 0: + classes += " outdated" + else: # xcp_ver > ref_xs_ver + classes += " better" + if row_classes == "notused" and xcp_ver: + classes += " unexpected" + + print(f'{xcp_ver_str}', file=outfile) + print(f'
', file=outfile) + + return 0 + +if __name__ == "__main__": + sys.exit(main()) From 84b5de61ab1660efe8f5217b55f8be36ce0a69d7 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 7 Mar 2025 16:15:51 +0100 Subject: [PATCH 04/16] WIP update waves --- scripts/rpmwatcher/repoquery.py | 5 +++-- scripts/rpmwatcher/yum_repo_status | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/rpmwatcher/repoquery.py b/scripts/rpmwatcher/repoquery.py index 51e6ce43..141a2ac8 100644 --- a/scripts/rpmwatcher/repoquery.py +++ b/scripts/rpmwatcher/repoquery.py @@ -212,12 +212,13 @@ def all_binrpms() -> set[str]: ret = set(run_repoquery(args)) return ret -def all_srpms() -> set[str]: +def all_srpms(all_versions: bool = False) -> set[str]: args = [ '--disablerepo=*', '--enablerepo=*-src', '--qf=%{name}-%{evr}' + QFNL, # to avoid getting the arch - '--latest-limit=1', # only most recent for each package '*', ] + if not all_versions: + args.append('--latest-limit=1') # only most recent for each package ret = set(run_repoquery(args)) return ret diff --git a/scripts/rpmwatcher/yum_repo_status b/scripts/rpmwatcher/yum_repo_status index 843830d9..2d61fa09 100755 --- a/scripts/rpmwatcher/yum_repo_status +++ b/scripts/rpmwatcher/yum_repo_status @@ -94,6 +94,10 @@ def collect_data_xs8(): in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xs8") for nevr in xs8_rpms_sources)} + logging.debug("get XS waves SRPM versions") + xs8_waves_srpms = {nevr for nevr in repoquery.all_srpms(all_versions=True) + if not FILTER_UPSTREAM or not repoquery.is_pristine_upstream(nevr)} + logging.info(f"xs8 src: {len(xs8_srpms_set)}") logging.info(f"xs8 bin: {len(xs8_rpms_sources_set)}") From a29247919bc9aeac9b3dc2224cdf1cfc499da9eb Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 7 Mar 2025 16:16:01 +0100 Subject: [PATCH 05/16] WIP column folding --- scripts/rpmwatcher/yum_repo_status | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/rpmwatcher/yum_repo_status b/scripts/rpmwatcher/yum_repo_status index 2d61fa09..e9c8a5cf 100755 --- a/scripts/rpmwatcher/yum_repo_status +++ b/scripts/rpmwatcher/yum_repo_status @@ -179,6 +179,9 @@ def main() -> int: + + + ', file=outfile) + print('', file=outfile) for srpm in sorted(all_srpms): xs_ver_src = xs8_srpms_set.get(srpm, None) @@ -249,7 +249,7 @@ def main() -> int: classes += " better" elif not ref_xs_ver: pass - elif rpm.labelCompare(xcp_ver, ref_xs_ver) < 0: + elif rpm.labelCompare(xcp_ver, ref_xs_ver) < 0: # type: ignore classes += " outdated" else: # xcp_ver > ref_xs_ver classes += " better" @@ -257,7 +257,7 @@ def main() -> int: classes += " unexpected" print(f'', file=outfile) + print('', file=outfile) print('
package XenServer 8 From ad5f2c20822c4a8bc0185623aeae3aa68a271cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 2 Jul 2025 16:30:24 +0200 Subject: [PATCH 06/16] Move to repo_status directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/.gitignore | 3 + scripts/repo_status/.python-version | 1 + .../package_status.csv | 0 scripts/repo_status/pyproject.toml | 49 +++ .../{rpmwatcher => repo_status}/repoquery.py | 0 .../{rpmwatcher => repo_status}/rpm_dep_tree | 0 scripts/repo_status/setup.cfg | 7 + scripts/{rpmwatcher => repo_status}/style.css | 0 scripts/repo_status/uv.lock | 372 ++++++++++++++++++ .../yum_repo_query | 0 .../yum_repo_status | 0 11 files changed, 432 insertions(+) create mode 100644 scripts/repo_status/.gitignore create mode 100644 scripts/repo_status/.python-version rename scripts/{rpmwatcher => repo_status}/package_status.csv (100%) create mode 100644 scripts/repo_status/pyproject.toml rename scripts/{rpmwatcher => repo_status}/repoquery.py (100%) rename scripts/{rpmwatcher => repo_status}/rpm_dep_tree (100%) create mode 100644 scripts/repo_status/setup.cfg rename scripts/{rpmwatcher => repo_status}/style.css (100%) create mode 100644 scripts/repo_status/uv.lock rename scripts/{rpmwatcher => repo_status}/yum_repo_query (100%) rename scripts/{rpmwatcher => repo_status}/yum_repo_status (100%) diff --git a/scripts/repo_status/.gitignore b/scripts/repo_status/.gitignore new file mode 100644 index 00000000..840b3c09 --- /dev/null +++ b/scripts/repo_status/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.dot +*.html diff --git a/scripts/repo_status/.python-version b/scripts/repo_status/.python-version new file mode 100644 index 00000000..2d4715b6 --- /dev/null +++ b/scripts/repo_status/.python-version @@ -0,0 +1 @@ +3.11.11 diff --git a/scripts/rpmwatcher/package_status.csv b/scripts/repo_status/package_status.csv similarity index 100% rename from scripts/rpmwatcher/package_status.csv rename to scripts/repo_status/package_status.csv diff --git a/scripts/repo_status/pyproject.toml b/scripts/repo_status/pyproject.toml new file mode 100644 index 00000000..1e9378b6 --- /dev/null +++ b/scripts/repo_status/pyproject.toml @@ -0,0 +1,49 @@ +[project] +name = "repo_status" +version = "0.1.0" +description = "Repository status and comparison" +readme = "README.md" +requires-python = "~=3.11" +dependencies = [ + "requests", + "rpm", +] + +[dependency-groups] +dev = [ + "icecream", + "mypy", + "flake8", + "pydocstyle", + "pyright", + "ruff", + "types-requests", + "typing-extensions", +] + +[tool.pyright] +typeCheckingMode = "standard" + +[tool.ruff] +preview = true +line-length = 120 +exclude = [".git"] + +[tool.ruff.format] +quote-style = "preserve" + +[tool.ruff.lint] +select = [ + "F", # Pyflakes + "I", # isort + "SLF", # flake8-self + "SIM", # flake8-simplify +] +# don't use some of the SIM rules +ignore = [ + "SIM105", # suppressible-exception + "SIM108", # if-else-block-instead-of-if-exp +] + +[tool.ruff.lint.isort] +lines-after-imports = 1 diff --git a/scripts/rpmwatcher/repoquery.py b/scripts/repo_status/repoquery.py similarity index 100% rename from scripts/rpmwatcher/repoquery.py rename to scripts/repo_status/repoquery.py diff --git a/scripts/rpmwatcher/rpm_dep_tree b/scripts/repo_status/rpm_dep_tree similarity index 100% rename from scripts/rpmwatcher/rpm_dep_tree rename to scripts/repo_status/rpm_dep_tree diff --git a/scripts/repo_status/setup.cfg b/scripts/repo_status/setup.cfg new file mode 100644 index 00000000..fb7256aa --- /dev/null +++ b/scripts/repo_status/setup.cfg @@ -0,0 +1,7 @@ +[flake8] +max-line-length=120 +ignore=E261,E302,E305,W503,F +exclude=.git,.venv + +[pydocstyle] +ignore=D100,D101,D102,D103,D104,D105,D106,D107,D203,D210,D212,D401,D403 diff --git a/scripts/rpmwatcher/style.css b/scripts/repo_status/style.css similarity index 100% rename from scripts/rpmwatcher/style.css rename to scripts/repo_status/style.css diff --git a/scripts/repo_status/uv.lock b/scripts/repo_status/uv.lock new file mode 100644 index 00000000..e0c26a3f --- /dev/null +++ b/scripts/repo_status/uv.lock @@ -0,0 +1,372 @@ +version = 1 +revision = 2 +requires-python = ">=3.11, <4" + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "icecream" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "colorama" }, + { name = "executing" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/5f/9635c20f14f558768e89b0e20c32ca852640b8973e102ff2f6ee229bbebc/icecream-2.1.5.tar.gz", hash = "sha256:14d21e3383326a69a8c1a3bcf11f83283459f0d269ece5af83fce2c0d663efec", size = 16744, upload-time = "2025-06-25T18:02:31.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/0a/9496191b82b2839cc92fc713a5888931ab301dfcc55b9d5a49665cdb2aa5/icecream-2.1.5-py3-none-any.whl", hash = "sha256:c020917c5cb180a528dbba170250ccd8b74ebc75f2a7a8e839819bf959b9f80c", size = 14378, upload-time = "2025-06-25T18:02:30.262Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mypy" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "snowballstemmer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/d5385ca59fd065e3c6a5fe19f9bc9d5ea7f2509fa8c9c22fb6b2031dd953/pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1", size = 36796, upload-time = "2023-01-17T20:29:19.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/ea/99ddefac41971acad68f14114f38261c1f27dac0b3ec529824ebc739bdaa/pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", size = 38038, upload-time = "2023-01-17T20:29:18.094Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.402" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207, upload-time = "2025-06-11T08:48:35.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004, upload-time = "2025-06-11T08:48:33.998Z" }, +] + +[[package]] +name = "repo-status" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "requests" }, + { name = "rpm" }, +] + +[package.dev-dependencies] +dev = [ + { name = "flake8" }, + { name = "icecream" }, + { name = "mypy" }, + { name = "pydocstyle" }, + { name = "pyright" }, + { name = "ruff" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "requests" }, + { name = "rpm" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "flake8" }, + { name = "icecream" }, + { name = "mypy" }, + { name = "pydocstyle" }, + { name = "pyright" }, + { name = "ruff" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rpm" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/ce/8db44d2b8fd6713a59e391d12b6816854b7bee8121ae7370c2d565de4265/rpm-0.4.0.tar.gz", hash = "sha256:79adbefa82318e2625d6e4fa16666cf88543498a1f2c10dc3879165d1dc3ecee", size = 11237, upload-time = "2025-04-08T08:57:26.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/96/0f4e9ba318c6e09b76357ee88d6c73101f8799f8ed707cfdc1df131b4234/rpm-0.4.0-py3-none-any.whl", hash = "sha256:0ef697cb5fb73bf9300a13d423529d7ec215239bf95c5ecb145e6610645f6067", size = 5151, upload-time = "2025-04-08T08:57:24.971Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/97/38/796a101608a90494440856ccfb52b1edae90de0b817e76bfade66b12d320/ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c", size = 4413426, upload-time = "2025-06-26T20:34:14.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/bf/3dba52c1d12ab5e78d75bd78ad52fb85a6a1f29cc447c2423037b82bed0d/ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b", size = 10305649, upload-time = "2025-06-26T20:33:39.242Z" }, + { url = "https://files.pythonhosted.org/packages/8c/65/dab1ba90269bc8c81ce1d499a6517e28fe6f87b2119ec449257d0983cceb/ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0", size = 11120201, upload-time = "2025-06-26T20:33:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3e/2d819ffda01defe857fa2dd4cba4d19109713df4034cc36f06bbf582d62a/ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be", size = 10466769, upload-time = "2025-06-26T20:33:44.102Z" }, + { url = "https://files.pythonhosted.org/packages/63/37/bde4cf84dbd7821c8de56ec4ccc2816bce8125684f7b9e22fe4ad92364de/ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff", size = 10660902, upload-time = "2025-06-26T20:33:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3a/390782a9ed1358c95e78ccc745eed1a9d657a537e5c4c4812fce06c8d1a0/ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d", size = 10167002, upload-time = "2025-06-26T20:33:47.81Z" }, + { url = "https://files.pythonhosted.org/packages/6d/05/f2d4c965009634830e97ffe733201ec59e4addc5b1c0efa035645baa9e5f/ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd", size = 11751522, upload-time = "2025-06-26T20:33:49.857Z" }, + { url = "https://files.pythonhosted.org/packages/35/4e/4bfc519b5fcd462233f82fc20ef8b1e5ecce476c283b355af92c0935d5d9/ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010", size = 12520264, upload-time = "2025-06-26T20:33:52.199Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/7756a6925da236b3a31f234b4167397c3e5f91edb861028a631546bad719/ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e", size = 12133882, upload-time = "2025-06-26T20:33:54.231Z" }, + { url = "https://files.pythonhosted.org/packages/dd/00/40da9c66d4a4d51291e619be6757fa65c91b92456ff4f01101593f3a1170/ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed", size = 11608941, upload-time = "2025-06-26T20:33:56.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/e7/f898391cc026a77fbe68dfea5940f8213622474cb848eb30215538a2dadf/ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc", size = 11602887, upload-time = "2025-06-26T20:33:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/0891872fc6aab8678084f4cf8826f85c5d2d24aa9114092139a38123f94b/ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9", size = 10521742, upload-time = "2025-06-26T20:34:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/2a/98/d6534322c74a7d47b0f33b036b2498ccac99d8d8c40edadb552c038cecf1/ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13", size = 10149909, upload-time = "2025-06-26T20:34:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/34/5c/9b7ba8c19a31e2b6bd5e31aa1e65b533208a30512f118805371dbbbdf6a9/ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c", size = 11136005, upload-time = "2025-06-26T20:34:04.723Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/9bbefa4d0ff2c000e4e533f591499f6b834346025e11da97f4ded21cb23e/ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6", size = 11648579, upload-time = "2025-06-26T20:34:06.766Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/20cdb593783f8f411839ce749ec9ae9e4298c2b2079b40295c3e6e2089e1/ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245", size = 10519495, upload-time = "2025-06-26T20:34:08.718Z" }, + { url = "https://files.pythonhosted.org/packages/cf/56/7158bd8d3cf16394928f47c637d39a7d532268cd45220bdb6cd622985760/ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013", size = 11547485, upload-time = "2025-06-26T20:34:11.008Z" }, + { url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209, upload-time = "2025-06-26T20:34:12.928Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] diff --git a/scripts/rpmwatcher/yum_repo_query b/scripts/repo_status/yum_repo_query similarity index 100% rename from scripts/rpmwatcher/yum_repo_query rename to scripts/repo_status/yum_repo_query diff --git a/scripts/rpmwatcher/yum_repo_status b/scripts/repo_status/yum_repo_status similarity index 100% rename from scripts/rpmwatcher/yum_repo_status rename to scripts/repo_status/yum_repo_status From 4d1663633e57231c2ebbe6d34a624160d9b97aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 2 Jul 2025 16:58:37 +0200 Subject: [PATCH 07/16] Style and lint fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/repoquery.py | 14 ++++++------- scripts/repo_status/rpm_dep_tree | 9 +++----- scripts/repo_status/yum_repo_status | 32 ++++++++++++++--------------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/scripts/repo_status/repoquery.py b/scripts/repo_status/repoquery.py index 141a2ac8..664c7748 100644 --- a/scripts/repo_status/repoquery.py +++ b/scripts/repo_status/repoquery.py @@ -70,7 +70,7 @@ def setup_xcpng_yum_repos(*, yum_repo_d: str, sections: Iterable[str], skip_if_unavailable=False """ -def setup_xs8_yum_repos(*, yum_repo_d: str, sections: Iterable[str])-> None: +def setup_xs8_yum_repos(*, yum_repo_d: str, sections: Iterable[str]) -> None: with open(os.path.join(yum_repo_d, "xs8.repo"), "w") as yumrepoconf: for section in sections: block = XS8_YUMREPO_TMPL.format(section=section) @@ -153,7 +153,7 @@ def srpm_strip_src_rpm(srpmname: str) -> str: assert srpmname.endswith(SUFFIX), f"{srpmname} does not end in .src.rpm" nrv = srpmname[:-len(SUFFIX)] return nrv - + def rpm_requires(rpmname: str) -> Sequence[str]: args = [ '--disablerepo=*-src', # else requires of same-name SRPM are included @@ -174,7 +174,7 @@ def srpm_requires(srpmname: str) -> set[str]: return ret def srpm_binrpms(srpmname: str) -> set[str]: - ret = SRPM_BINRPMS_CACHE.get(srpmname, None) + ret = SRPM_BINRPMS_CACHE.get(srpmname) if ret is None: # FIXME should not happen logging.error("%r not found in cache", srpmname) assert False @@ -184,13 +184,11 @@ def srpm_binrpms(srpmname: str) -> set[str]: UPSTREAM_REGEX = re.compile(r'\.el[0-9]+(_[0-9]+)?(\..*|)$') RPM_NVR_SPLIT_REGEX = re.compile(r'^(.+)-([^-]+)-([^-]+)$') -def is_pristine_upstream(rpmname:str) -> bool: - if re.search(UPSTREAM_REGEX, rpmname): - return True - return False +def is_pristine_upstream(rpmname: str) -> bool: + return bool(re.search(UPSTREAM_REGEX, rpmname)) def rpm_parse_nevr(nevr: str, suffix: str) -> tuple[str, str, str, str]: - "Parse into (name, epoch:version, release) stripping suffix from release" + """Parse into (name, epoch:version, release) stripping suffix from release.""" m = re.match(RPM_NVR_SPLIT_REGEX, nevr) assert m, f"{nevr} does not match NEVR pattern" n, ev, r = m.groups() diff --git a/scripts/repo_status/rpm_dep_tree b/scripts/repo_status/rpm_dep_tree index c0e3cf43..aa411141 100755 --- a/scripts/repo_status/rpm_dep_tree +++ b/scripts/repo_status/rpm_dep_tree @@ -1,6 +1,5 @@ #! /usr/bin/env python3 -import atexit import logging import re import sys @@ -18,11 +17,9 @@ def is_upstream(rpmname: str) -> bool: return True m = re.match(repoquery.RPM_NVR_SPLIT_REGEX, rpmname) assert m, f"{rpmname!r} does not match {repoquery.RPM_NVR_SPLIT_REGEX!r}" - if m.group(1) in ['systemd', 'util-linux', 'ncurses', - #'xapi', - 'devtoolset-11-gcc', 'devtoolset-11-binutils']: - return True - return False + return m.group(1) in ['systemd', 'util-linux', 'ncurses', + # 'xapi', + 'devtoolset-11-gcc', 'devtoolset-11-binutils'] def main() -> int: logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.DEBUG) diff --git a/scripts/repo_status/yum_repo_status b/scripts/repo_status/yum_repo_status index e9c8a5cf..7ceb5557 100755 --- a/scripts/repo_status/yum_repo_status +++ b/scripts/repo_status/yum_repo_status @@ -1,14 +1,14 @@ #! /usr/bin/env python3 import argparse -from collections import namedtuple, OrderedDict import csv import logging -import os -import rpm # type: ignore import sys import tempfile -from typing import Iterable, Iterator, Literal, Sequence +from collections import OrderedDict, namedtuple +from typing import Iterable, Iterator, Literal + +import rpm # type: ignore import repoquery @@ -17,7 +17,7 @@ XCP_VERSION = "8.3" FILTER_UPSTREAM = False def evr_format(evr: tuple[str, str, str] | None | Literal['=']) -> str: - if evr == "=": # dirty special case + if isinstance(evr, str) and evr == "=": # dirty special case return evr if evr is None: return "-" @@ -29,7 +29,7 @@ def filter_best_evr(nevrs: Iterable[tuple[str, str, str, str]] ) -> Iterator[tuple[str, str, str, str]]: best: dict[str, tuple[str, str, str]] = {} for (n, e, v, r) in nevrs: - if n not in best or rpm.labelCompare(best[n], (e, v, r)) < 0: + if n not in best or rpm.labelCompare(best[n], (e, v, r)) < 0: # type: ignore best[n] = (e, v, r) yield (n, e, v, r) # else (e, v, r) is older than a previously-seen version, drop @@ -41,8 +41,8 @@ def collect_data_xcpng() -> OrderedDict: ("testing", ["testing"]), ("ci", ["ci"]), ("incoming", ["incoming"]), - #("ydi1", ["ydi1"]), - ("dtt1", ["dtt1"]), + # ("ydi1", ["ydi1"]), + # ("dtt1", ["dtt1"]), ): with (tempfile.NamedTemporaryFile() as dnfconf, tempfile.TemporaryDirectory() as yumrepod): @@ -52,7 +52,7 @@ def collect_data_xcpng() -> OrderedDict: bin_arch=None, version=XCP_VERSION) repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) - #repoquery.fill_srpm_binrpms_cache() + # repoquery.fill_srpm_binrpms_cache() logging.debug("get all XCP-ng %s SRPMs", label) xcp_srpms = {nevr for nevr in repoquery.all_srpms() @@ -82,16 +82,16 @@ def collect_data_xs8(): logging.debug("get all XS SRPMs") xs8_srpms = {nevr for nevr in repoquery.all_srpms() if not FILTER_UPSTREAM or not repoquery.is_pristine_upstream(nevr)} - xs8_rpms_sources = {nevr for nevr in repoquery.SRPM_BINRPMS_CACHE.keys() + xs8_rpms_sources = {nevr for nevr in repoquery.SRPM_BINRPMS_CACHE if not FILTER_UPSTREAM or not repoquery.is_pristine_upstream(nevr)} xs8_srpms_set = {n: (e, v, r) for (n, e, v, r) - in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xs8") + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") for nevr in xs8_srpms)} xs8_rpms_sources_set = {n: (e, v, r) for (n, e, v, r) - in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xs8") + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") for nevr in xs8_rpms_sources)} logging.debug("get XS waves SRPM versions") @@ -120,7 +120,7 @@ def collect_data_xs8ea(): xs8ea_srpms_set = {n: (e, v, r) for (n, e, v, r) - in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xs8") + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") for nevr in xs8ea_srpms)} logging.info(f"xs8 EA src: {len(xs8ea_srpms_set)}") @@ -191,7 +191,7 @@ def main() -> int: print('
srcrpmbinrpmEA srcrpm', file=outfile) for label in xcp_sets: print(f'{label}', file=outfile) - print(f'
{xcp_ver_str}', file=outfile) - print(f'
', file=outfile) return 0 From 642739ada998c100a2473dff660c516eaa4c43cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 2 Jul 2025 16:58:57 +0200 Subject: [PATCH 08/16] Fix dnf version detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit newer dnf versions are showing a lot of stuff in dnf --version Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/repoquery.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/repo_status/repoquery.py b/scripts/repo_status/repoquery.py index 664c7748..51f6484b 100644 --- a/scripts/repo_status/repoquery.py +++ b/scripts/repo_status/repoquery.py @@ -23,8 +23,10 @@ """ # DNF v4 adds an implicit trailing newline to --qf format, but v5 does not -dnf_version = subprocess.check_output(['dnf', '--version'], universal_newlines=True).strip().split('.') -if int(dnf_version[0]) >= 5: +dnf_version = re.search(r"([0-9]+)\.[0-9.]+", + subprocess.check_output(['dnf', '--version'], universal_newlines=True).splitlines()[0]) +assert dnf_version is not None +if int(dnf_version[1]) >= 5: QFNL = "\n" else: QFNL = "" From ed387bef6cf83207dd6e782b98b5f894732e31ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 2 Jul 2025 17:37:25 +0200 Subject: [PATCH 09/16] Use argparse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit where it makes sense Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/rpm_dep_tree | 17 ++++++++++++----- scripts/repo_status/yum_repo_status | 7 +------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/repo_status/rpm_dep_tree b/scripts/repo_status/rpm_dep_tree index aa411141..db92d3c1 100755 --- a/scripts/repo_status/rpm_dep_tree +++ b/scripts/repo_status/rpm_dep_tree @@ -1,5 +1,6 @@ #! /usr/bin/env python3 +import argparse import logging import re import sys @@ -22,18 +23,24 @@ def is_upstream(rpmname: str) -> bool: 'devtoolset-11-gcc', 'devtoolset-11-binutils'] def main() -> int: - logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.DEBUG) - this_exe, version, root_srpm = sys.argv + parser = argparse.ArgumentParser(description="Generate a graph of a package dependencies") + parser.add_argument('version', help='XCP-ng version to query (8.2, 8.3, 9.0, …)') + parser.add_argument('srpm', help='Source rpm to query') + parser.add_argument('-v', '--verbose', action='count', default=0) + args = parser.parse_args() + + loglevel = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}.get(args.verbose, logging.DEBUG) + logging.basicConfig(format='[%(levelname)s] %(message)s', level=loglevel) with (tempfile.NamedTemporaryFile() as dnfconf, tempfile.TemporaryDirectory() as yumrepod, - open(f"{root_srpm}-{version}.dot", "w") as dotfile): + open(f"{args.srpm}-{args.version}.dot", "w") as dotfile): repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, sections=['base', 'updates'], bin_arch=ARCH, - version=version) + version=args.version) repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) repoquery.fill_srpm_binrpms_cache() @@ -42,7 +49,7 @@ def main() -> int: print("digraph packages {", file=dotfile) srpms_seen: set[str] = set() - new_srpms = {repoquery.srpm_nevr(root_srpm)} + new_srpms = {repoquery.srpm_nevr(args.srpm)} while new_srpms: next_srpms = set() # preparing next round's new_srpms logging.info("seen: %s, new: %s", len(srpms_seen), len(new_srpms)) diff --git a/scripts/repo_status/yum_repo_status b/scripts/repo_status/yum_repo_status index 7ceb5557..03f94e67 100755 --- a/scripts/repo_status/yum_repo_status +++ b/scripts/repo_status/yum_repo_status @@ -140,13 +140,8 @@ def main() -> int: parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', action='count', default=0) args = parser.parse_args() - if args.verbose > 2: - args.verbose = 2 - loglevel = {0: logging.WARNING, - 1: logging.INFO, - 2: logging.DEBUG, - }[args.verbose] + loglevel = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}.get(args.verbose, logging.DEBUG) logging.basicConfig(format='[%(levelname)s] %(message)s', level=loglevel) PACKAGE_STATUS = read_package_status_metadata() From 4f22c9ce1994c2424bc1bf8d365f591b08a71ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 3 Jul 2025 09:39:25 +0200 Subject: [PATCH 10/16] Don't use global, local vars in lowercase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/repoquery.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/scripts/repo_status/repoquery.py b/scripts/repo_status/repoquery.py index 51f6484b..eeff39bd 100644 --- a/scripts/repo_status/repoquery.py +++ b/scripts/repo_status/repoquery.py @@ -108,34 +108,29 @@ def fill_srpm_binrpms_cache() -> None: '--qf', '%{name}-%{version}-%{release}.src.rpm,%{name}-%{evr}' + QFNL, '--latest-limit=1', ] - SRPM_NEVR_CACHE = { # sourcerpm -> srpm-nevr - sourcerpm: nevr - for sourcerpm, nevr in (line.split(',') - for line in run_repoquery(args)) - } + + # sourcerpm -> srpm-nevr + srpm_nevr_cache = dict(line.split(',') for line in run_repoquery(args)) # binary -> source mapping logging.debug("get binary to source mapping") - global SRPM_BINRPMS_CACHE, BINRPM_SOURCE_CACHE + BINRPM_SOURCE_CACHE.clear() args = [ '--disablerepo=*-src', '*', '--qf', '%{name}-%{evr},%{sourcerpm}' + QFNL, # FIXME no epoch in sourcerpm, why does it work? '--latest-limit=1', ] - BINRPM_SOURCE_CACHE = { + BINRPM_SOURCE_CACHE.update({ # packages without source are not in SRPM_NEVR_CACHE, fallback to sourcerpm - binrpm: SRPM_NEVR_CACHE.get(sourcerpm, srpm_strip_src_rpm(sourcerpm)) + binrpm: srpm_nevr_cache.get(sourcerpm, srpm_strip_src_rpm(sourcerpm)) for binrpm, sourcerpm in (line.split(',') for line in run_repoquery(args)) - } + }) # reverse mapping source -> binaries - SRPM_BINRPMS_CACHE = {} + SRPM_BINRPMS_CACHE.clear() for binrpm, srpm in BINRPM_SOURCE_CACHE.items(): - binrpms = SRPM_BINRPMS_CACHE.get(srpm, set()) - if not binrpms: - SRPM_BINRPMS_CACHE[srpm] = binrpms - binrpms.add(binrpm) + SRPM_BINRPMS_CACHE.setdefault(srpm, set()).add(binrpm) def srpm_nevr(rpmname: str) -> str: args = [ From 79627acdf1a2057dbac5f5ec39fcc2101eb90e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 3 Jul 2025 09:41:26 +0200 Subject: [PATCH 11/16] Fix flags for dnf 5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/repoquery.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/repo_status/repoquery.py b/scripts/repo_status/repoquery.py index eeff39bd..f5c8d3aa 100644 --- a/scripts/repo_status/repoquery.py +++ b/scripts/repo_status/repoquery.py @@ -28,8 +28,10 @@ assert dnf_version is not None if int(dnf_version[1]) >= 5: QFNL = "\n" + REQUIRES_FLAGS = ["--providers-of=requires"] else: QFNL = "" + REQUIRES_FLAGS = ["--resolve", "--requires"] def setup_xcpng_yum_repos(*, yum_repo_d: str, sections: Iterable[str], bin_arch: str | None, version: str) -> None: @@ -155,8 +157,8 @@ def rpm_requires(rpmname: str) -> Sequence[str]: args = [ '--disablerepo=*-src', # else requires of same-name SRPM are included '--qf=%{name}-%{evr}' + QFNL, # to avoid getting the arch and explicit zero epoch - '--resolve', - '--requires', rpmname, + ] + REQUIRES_FLAGS + [ + rpmname, ] ret = run_repoquery(args) return ret @@ -164,8 +166,8 @@ def rpm_requires(rpmname: str) -> Sequence[str]: def srpm_requires(srpmname: str) -> set[str]: args = [ '--qf=%{name}-%{evr}' + QFNL, # to avoid getting the arch - '--resolve', - '--requires', f"{srpmname}.src", + ] + REQUIRES_FLAGS + [ + f"{srpmname}.src", ] ret = set(run_repoquery(args)) return ret From 573885f93e5b727f49c06900447f68b1ada48976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 3 Jul 2025 09:58:04 +0200 Subject: [PATCH 12/16] Find packages to update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/create_rebase_cards.py | 132 +++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100755 scripts/repo_status/create_rebase_cards.py diff --git a/scripts/repo_status/create_rebase_cards.py b/scripts/repo_status/create_rebase_cards.py new file mode 100755 index 00000000..51bc8288 --- /dev/null +++ b/scripts/repo_status/create_rebase_cards.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import logging +import tempfile +from collections import namedtuple +from typing import Iterable, Iterator + +import rpm # type: ignore + +import repoquery + +ARCH = "x86_64" +XCP_VERSION = "8.3" + +class EVR: + def __init__(self, e: str, v: str, r: str): + self._evr = (e, v, r) + + def __eq__(self, other): + if isinstance(other, EVR): + return self._evr == other._evr + else: + return self._evr == other + + def __gt__(self, other): + if isinstance(other, EVR): + return rpm.labelCompare(self._evr, other._evr) > 0 # type: ignore + else: + return self._evr > other + + def __lt__(self, other): + return other > self + + def __str__(self): + if self._evr[0] != '0': + return f'{self._evr[0]}:{self._evr[1]}.{self._evr[2]}' + else: + return f'{self._evr[1]}.{self._evr[2]}' + +# Filters an iterator of (n, e, v, r) for newest evr of each `n`. +# Older versions are allowed to appear before the newer ones. +def filter_best_evr(nevrs: Iterable[tuple[str, str, str, str]]) -> Iterator[tuple[str, str, str, str]]: + best: dict[str, tuple[str, str, str]] = {} + for (n, e, v, r) in nevrs: + if n not in best or rpm.labelCompare(best[n], (e, v, r)) < 0: # type: ignore + best[n] = (e, v, r) + yield (n, e, v, r) + # else (e, v, r) is older than a previously-seen version, drop + +def collect_data_xcpng() -> dict[str, EVR]: + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'updates'], + bin_arch=None, + version=XCP_VERSION) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + + xcp_nevr = { + n: EVR(e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xcpng{XCP_VERSION}") + for nevr in repoquery.all_srpms())} + + return xcp_nevr + +def collect_data_xs8(): + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + + repoquery.setup_xs8_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'normal'], + ) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + logging.debug("fill cache with XS info") + repoquery.fill_srpm_binrpms_cache() + + logging.debug("get all XS SRPMs") + xs8_srpms = {nevr for nevr in repoquery.all_srpms()} + xs8_rpms_sources = {nevr for nevr in repoquery.SRPM_BINRPMS_CACHE} + + xs8_srpms_set = {n: EVR(e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") + for nevr in xs8_srpms)} + xs8_rpms_sources_set = {n: EVR(e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") + for nevr in xs8_rpms_sources)} + + return (xs8_srpms_set, xs8_rpms_sources_set) + +def read_package_status_metadata(): + with open('package_status.csv', newline='') as csvfile: + csvreader = csv.reader(csvfile, delimiter=';', quotechar='|') + headers = next(csvreader) + assert headers == ["SRPM_name", "status", "comment"], f"unexpected headers {headers!r}" + PackageStatus = namedtuple("PackageStatus", headers[1:]) # type: ignore[misc] + return {row[0]: PackageStatus(*row[1:]) + for row in csvreader} + +parser = argparse.ArgumentParser() +parser.add_argument('-v', '--verbose', action='count', default=0) +args = parser.parse_args() + +loglevel = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}.get(args.verbose, logging.DEBUG) +logging.basicConfig(format='[%(levelname)s] %(message)s', level=loglevel) + +PACKAGE_STATUS = read_package_status_metadata() + +xcp_set = collect_data_xcpng() +(xs8_srpms_set, xs8_rpms_sources_set) = collect_data_xs8() + +for n in sorted(set(xs8_srpms_set.keys()) | xs8_rpms_sources_set.keys()): + if n in PACKAGE_STATUS and PACKAGE_STATUS[n].status == 'ignored': + logging.debug(f"ignoring {n}") + continue + xs8_srpms_evr = xs8_srpms_set.get(n) + xs8_rpms_sources_evr = xs8_rpms_sources_set.get(n) + if xs8_srpms_evr is not None and xs8_rpms_sources_evr is not None: + xs8_evr = max(xs8_srpms_evr, xs8_rpms_sources_evr) + else: + xs8_evr = xs8_srpms_evr or xs8_rpms_sources_evr + xcp_evr = xcp_set.get(n) + # if xcp_evr is not None and xcp_evr < xs8_evr: + if xcp_evr is None: + if not repoquery.is_pristine_upstream(str(xs8_evr)): + print(f'{n} {xcp_evr} -> {xs8_evr}') + elif xcp_evr < xs8_evr: + print(f'{n} {xcp_evr} -> {xs8_evr}') From df770758b1ce0f8fee978578e4ac219bbcaeb922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 3 Jul 2025 14:00:16 +0200 Subject: [PATCH 13/16] Show XS8 update that introduced a package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/create_rebase_cards.py | 107 ++-------------- scripts/repo_status/lib.py | 138 +++++++++++++++++++++ scripts/repo_status/pyproject.toml | 1 + scripts/repo_status/repoquery.py | 4 +- scripts/repo_status/uv.lock | 11 ++ 5 files changed, 163 insertions(+), 98 deletions(-) create mode 100755 scripts/repo_status/lib.py diff --git a/scripts/repo_status/create_rebase_cards.py b/scripts/repo_status/create_rebase_cards.py index 51bc8288..6aada568 100755 --- a/scripts/repo_status/create_rebase_cards.py +++ b/scripts/repo_status/create_rebase_cards.py @@ -1,105 +1,13 @@ #!/usr/bin/env python3 import argparse -import csv import logging -import tempfile -from collections import namedtuple -from typing import Iterable, Iterator -import rpm # type: ignore +# from icecream import ic +from tabulate import tabulate import repoquery - -ARCH = "x86_64" -XCP_VERSION = "8.3" - -class EVR: - def __init__(self, e: str, v: str, r: str): - self._evr = (e, v, r) - - def __eq__(self, other): - if isinstance(other, EVR): - return self._evr == other._evr - else: - return self._evr == other - - def __gt__(self, other): - if isinstance(other, EVR): - return rpm.labelCompare(self._evr, other._evr) > 0 # type: ignore - else: - return self._evr > other - - def __lt__(self, other): - return other > self - - def __str__(self): - if self._evr[0] != '0': - return f'{self._evr[0]}:{self._evr[1]}.{self._evr[2]}' - else: - return f'{self._evr[1]}.{self._evr[2]}' - -# Filters an iterator of (n, e, v, r) for newest evr of each `n`. -# Older versions are allowed to appear before the newer ones. -def filter_best_evr(nevrs: Iterable[tuple[str, str, str, str]]) -> Iterator[tuple[str, str, str, str]]: - best: dict[str, tuple[str, str, str]] = {} - for (n, e, v, r) in nevrs: - if n not in best or rpm.labelCompare(best[n], (e, v, r)) < 0: # type: ignore - best[n] = (e, v, r) - yield (n, e, v, r) - # else (e, v, r) is older than a previously-seen version, drop - -def collect_data_xcpng() -> dict[str, EVR]: - with (tempfile.NamedTemporaryFile() as dnfconf, - tempfile.TemporaryDirectory() as yumrepod): - repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, - sections=['base', 'updates'], - bin_arch=None, - version=XCP_VERSION) - repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) - - xcp_nevr = { - n: EVR(e, v, r) - for (n, e, v, r) - in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xcpng{XCP_VERSION}") - for nevr in repoquery.all_srpms())} - - return xcp_nevr - -def collect_data_xs8(): - with (tempfile.NamedTemporaryFile() as dnfconf, - tempfile.TemporaryDirectory() as yumrepod): - - repoquery.setup_xs8_yum_repos(yum_repo_d=yumrepod, - sections=['base', 'normal'], - ) - repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) - logging.debug("fill cache with XS info") - repoquery.fill_srpm_binrpms_cache() - - logging.debug("get all XS SRPMs") - xs8_srpms = {nevr for nevr in repoquery.all_srpms()} - xs8_rpms_sources = {nevr for nevr in repoquery.SRPM_BINRPMS_CACHE} - - xs8_srpms_set = {n: EVR(e, v, r) - for (n, e, v, r) - in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") - for nevr in xs8_srpms)} - xs8_rpms_sources_set = {n: EVR(e, v, r) - for (n, e, v, r) - in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") - for nevr in xs8_rpms_sources)} - - return (xs8_srpms_set, xs8_rpms_sources_set) - -def read_package_status_metadata(): - with open('package_status.csv', newline='') as csvfile: - csvreader = csv.reader(csvfile, delimiter=';', quotechar='|') - headers = next(csvreader) - assert headers == ["SRPM_name", "status", "comment"], f"unexpected headers {headers!r}" - PackageStatus = namedtuple("PackageStatus", headers[1:]) # type: ignore[misc] - return {row[0]: PackageStatus(*row[1:]) - for row in csvreader} +from lib import collect_data_xcpng, collect_data_xs8, get_xs8_rpm_updates, read_package_status_metadata parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', action='count', default=0) @@ -112,7 +20,9 @@ def read_package_status_metadata(): xcp_set = collect_data_xcpng() (xs8_srpms_set, xs8_rpms_sources_set) = collect_data_xs8() +srpm_updates = get_xs8_rpm_updates() +res = [] for n in sorted(set(xs8_srpms_set.keys()) | xs8_rpms_sources_set.keys()): if n in PACKAGE_STATUS and PACKAGE_STATUS[n].status == 'ignored': logging.debug(f"ignoring {n}") @@ -124,9 +34,12 @@ def read_package_status_metadata(): else: xs8_evr = xs8_srpms_evr or xs8_rpms_sources_evr xcp_evr = xcp_set.get(n) + xs8_update = srpm_updates.get(f'{n}-{xs8_evr}.xs8', '?') # if xcp_evr is not None and xcp_evr < xs8_evr: if xcp_evr is None: if not repoquery.is_pristine_upstream(str(xs8_evr)): - print(f'{n} {xcp_evr} -> {xs8_evr}') + res.append((xs8_update, n, xcp_evr, xs8_evr)) elif xcp_evr < xs8_evr: - print(f'{n} {xcp_evr} -> {xs8_evr}') + res.append((xs8_update, n, xcp_evr, xs8_evr)) +res.sort() +print(tabulate(res, headers=['xs8 update', 'SRPM', 'XCP-ng version', 'XS8 version'])) diff --git a/scripts/repo_status/lib.py b/scripts/repo_status/lib.py new file mode 100755 index 00000000..25317643 --- /dev/null +++ b/scripts/repo_status/lib.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +import csv +import gzip +import logging +import tempfile +import xml.etree.ElementTree as ET +from collections import namedtuple +from typing import Iterable, Iterator +from urllib.request import urlopen + +import rpm # type: ignore + +import repoquery + +ARCH = "x86_64" +XCP_VERSION = "8.3" + +class EVR: + def __init__(self, e: str, v: str, r: str): + self._evr = ('0' if e in [None, 'None'] else e, v, r) + + def __eq__(self, other): + if isinstance(other, EVR): + return self._evr == other._evr + else: + return self._evr == other + + def __gt__(self, other): + if isinstance(other, EVR): + return rpm.labelCompare(self._evr, other._evr) > 0 # type: ignore + else: + return self._evr > other + + def __lt__(self, other): + return other > self + + def __str__(self): + if self._evr[0] != '0': + return f'{self._evr[0]}:{self._evr[1]}-{self._evr[2]}' + else: + return f'{self._evr[1]}-{self._evr[2]}' + +# Filters an iterator of (n, e, v, r) for newest evr of each `n`. +# Older versions are allowed to appear before the newer ones. +def filter_best_evr(nevrs: Iterable[tuple[str, str, str, str]]) -> Iterator[tuple[str, str, str, str]]: + best: dict[str, tuple[str, str, str]] = {} + for (n, e, v, r) in nevrs: + if n not in best or rpm.labelCompare(best[n], (e, v, r)) < 0: # type: ignore + best[n] = (e, v, r) + yield (n, e, v, r) + # else (e, v, r) is older than a previously-seen version, drop + +def collect_data_xcpng() -> dict[str, EVR]: + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'updates'], + bin_arch=None, + version=XCP_VERSION) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + + xcp_nevr = { + n: EVR(e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, f".xcpng{XCP_VERSION}") + for nevr in repoquery.all_srpms())} + + return xcp_nevr + +def collect_data_xs8(): + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + + repoquery.setup_xs8_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'normal'], + ) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + logging.debug("fill cache with XS info") + repoquery.fill_srpm_binrpms_cache() + + logging.debug("get all XS SRPMs") + xs8_srpms = {nevr for nevr in repoquery.all_srpms()} + xs8_rpms_sources = {nevr for nevr in repoquery.SRPM_BINRPMS_CACHE} + + xs8_srpms_set = {n: EVR(e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") + for nevr in xs8_srpms)} + xs8_rpms_sources_set = {n: EVR(e, v, r) + for (n, e, v, r) + in filter_best_evr(repoquery.rpm_parse_nevr(nevr, ".xs8") + for nevr in xs8_rpms_sources)} + + return (xs8_srpms_set, xs8_rpms_sources_set) + +def read_package_status_metadata(): + with open('package_status.csv', newline='') as csvfile: + csvreader = csv.reader(csvfile, delimiter=';', quotechar='|') + headers = next(csvreader) + assert headers == ["SRPM_name", "status", "comment"], f"unexpected headers {headers!r}" + PackageStatus = namedtuple("PackageStatus", headers[1:]) # type: ignore[misc] + return {row[0]: PackageStatus(*row[1:]) + for row in csvreader} + +def get_xs8_rpm_updates(): + NS = {'repo': 'http://linux.duke.edu/metadata/repo'} + BASE_URL = 'http://repos/repos/XS8/normal/xs8p-normal' + + # read the update info path from repomd.xml + with urlopen(f'{BASE_URL}/repodata/repomd.xml') as f: + repomd = f.read() + data = ET.fromstring(repomd).find("repo:data[@type='updateinfo']", NS) + assert data is not None + location = data.find('repo:location', NS) + assert location is not None + path = location.attrib['href'] + + # read the update info file + res = {} + with urlopen(f'{BASE_URL}/{path}') as cf, gzip.open(cf, 'rb') as f: + updateinfo = f.read() + updates = ET.fromstring(updateinfo).findall('update') + for update in updates: + update_id = update.find('id') + assert update_id is not None + update_id = update_id.text + pkglist = update.find('pkglist') + assert pkglist is not None + collection = pkglist.find('collection') + assert collection is not None + packages = collection.findall('package') + for package in packages: + evr = EVR(package.attrib['epoch'], package.attrib['version'], package.attrib['release']) + rpm = f'{package.attrib["name"]}-{evr}' + srpm = repoquery.rpm_source_package(rpm, default=rpm) + res[srpm] = update_id + return res diff --git a/scripts/repo_status/pyproject.toml b/scripts/repo_status/pyproject.toml index 1e9378b6..3a661f0f 100644 --- a/scripts/repo_status/pyproject.toml +++ b/scripts/repo_status/pyproject.toml @@ -7,6 +7,7 @@ requires-python = "~=3.11" dependencies = [ "requests", "rpm", + "tabulate", ] [dependency-groups] diff --git a/scripts/repo_status/repoquery.py b/scripts/repo_status/repoquery.py index f5c8d3aa..3c931927 100644 --- a/scripts/repo_status/repoquery.py +++ b/scripts/repo_status/repoquery.py @@ -90,7 +90,9 @@ def dnf_setup(*, dnf_conf: str, yum_repo_d: str) -> None: ] BINRPM_SOURCE_CACHE: dict[str, str] = {} -def rpm_source_package(rpmname: str) -> str: +def rpm_source_package(rpmname: str, **kwargs) -> str: + if 'default' in kwargs: + return BINRPM_SOURCE_CACHE.get(rpmname, kwargs['default']) return BINRPM_SOURCE_CACHE[rpmname] def run_repoquery(args: list[str], split: bool = True) -> str | Sequence[str]: diff --git a/scripts/repo_status/uv.lock b/scripts/repo_status/uv.lock index e0c26a3f..8de7bfea 100644 --- a/scripts/repo_status/uv.lock +++ b/scripts/repo_status/uv.lock @@ -251,6 +251,7 @@ source = { virtual = "." } dependencies = [ { name = "requests" }, { name = "rpm" }, + { name = "tabulate" }, ] [package.dev-dependencies] @@ -269,6 +270,7 @@ dev = [ requires-dist = [ { name = "requests" }, { name = "rpm" }, + { name = "tabulate" }, ] [package.metadata.requires-dev] @@ -341,6 +343,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + [[package]] name = "types-requests" version = "2.32.4.20250611" From 17d9cb0eae89a388c449ce7b4445bac7e81fc086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Fri, 4 Jul 2025 14:51:53 +0200 Subject: [PATCH 14/16] Add dnf wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit to query the XCP-ng and XS8 repositories Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/dnf | 35 ++++++++++++++++++++++++++++++++ scripts/repo_status/repoquery.py | 7 +++++-- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100755 scripts/repo_status/dnf diff --git a/scripts/repo_status/dnf b/scripts/repo_status/dnf new file mode 100755 index 00000000..b671f0cf --- /dev/null +++ b/scripts/repo_status/dnf @@ -0,0 +1,35 @@ +#! /usr/bin/env python3 + +import logging +import sys +import tempfile + +import repoquery + +ARCH = "x86_64" +XCP_VERSION = "8.3" + +# Use `dnf` on xs8 and xcpng8.3 repos +def main() -> int: + logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO) + + args = sys.argv[1:] + + with (tempfile.NamedTemporaryFile() as dnfconf, + tempfile.TemporaryDirectory() as yumrepod): + + repoquery.setup_xcpng_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'updates'], + bin_arch=ARCH, + version=XCP_VERSION) + repoquery.setup_xs8_yum_repos(yum_repo_d=yumrepod, + sections=['base', 'normal', 'earlyaccess'], + ) + repoquery.dnf_setup(dnf_conf=dnfconf.name, yum_repo_d=yumrepod) + + print(repoquery.run_dnf(args, split=False)) + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/repo_status/repoquery.py b/scripts/repo_status/repoquery.py index 3c931927..1ce9e296 100644 --- a/scripts/repo_status/repoquery.py +++ b/scripts/repo_status/repoquery.py @@ -95,14 +95,17 @@ def rpm_source_package(rpmname: str, **kwargs) -> str: return BINRPM_SOURCE_CACHE.get(rpmname, kwargs['default']) return BINRPM_SOURCE_CACHE[rpmname] -def run_repoquery(args: list[str], split: bool = True) -> str | Sequence[str]: +def run_dnf(args: list[str], split: bool = True) -> str | Sequence[str]: assert DNF_BASE_CMD is not None - cmd = DNF_BASE_CMD + ['repoquery'] + args + cmd = DNF_BASE_CMD + args logging.debug('$ %s', ' '.join(cmd)) output = subprocess.check_output(cmd, universal_newlines=True).strip() logging.debug('> %s', output) return output.split() if split else output +def run_repoquery(args: list[str], split: bool = True) -> str | Sequence[str]: + return run_dnf(['repoquery'] + args, split) + SRPM_BINRPMS_CACHE: dict[str, set[str]] = {} # binrpm-nevr -> srpm-nevr def fill_srpm_binrpms_cache() -> None: # HACK: get nevr for what dnf outputs as %{sourcerpm} From d91dc5d95dee477bcf5af0d7097cb242a69bbdd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Fri, 4 Jul 2025 15:33:38 +0200 Subject: [PATCH 15/16] WIP: Exclude previously seen packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this needs actually needs to accurately store the packages when we see them, as they may be removed later Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/XS8-normal-src.txt | 119 +++++++++++++++++++++ scripts/repo_status/create_rebase_cards.py | 12 ++- scripts/repo_status/lib.py | 5 + 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 scripts/repo_status/XS8-normal-src.txt diff --git a/scripts/repo_status/XS8-normal-src.txt b/scripts/repo_status/XS8-normal-src.txt new file mode 100644 index 00000000..14fac443 --- /dev/null +++ b/scripts/repo_status/XS8-normal-src.txt @@ -0,0 +1,119 @@ +XS8-normal-src/auto-cert-kit-8.4.5-1.xs8.src.rpm +XS8-normal-src/biosdevname-0.3.10-5.xs8.src.rpm +XS8-normal-src/blktap-3.55.5-2.xs8.src.rpm +XS8-normal-src/bpftool-7.4.0-2.xs8.src.rpm +XS8-normal-src/broadcom-bnxt-en-1.10.2_223.0.183.0-2.xs8.src.rpm +XS8-normal-src/broadcom-mpi3mr-8.1.4.0.0-2.xs8.src.rpm +XS8-normal-src/busybox-1.22.1-7.xs8.src.rpm +XS8-normal-src/cisco-enic-4.5.0.7-1.xs8.src.rpm +XS8-normal-src/cisco-fnic-2.0.0.90-1.xs8.src.rpm +XS8-normal-src/cmake3-3.26.4-3.xs8.src.rpm +XS8-normal-src/compiler-rt18-18.1.8-3.xs8.src.rpm +XS8-normal-src/curl-8.6.0-2.xs8.src.rpm +XS8-normal-src/devtoolset-11-gcc-11.2.1-6.xs8.src.rpm +XS8-normal-src/dlm-4.0.7-2.xs8.src.rpm +XS8-normal-src/dwarves-1.26-3.xs8.src.rpm +XS8-normal-src/e2fsprogs-1.47.0-1.xs8.src.rpm +XS8-normal-src/edk2-20220801-1.7.7.xs8.src.rpm +XS8-normal-src/gdisk-1.0.10-1.xs8.src.rpm +XS8-normal-src/golang-1.19.7-1.xs8.src.rpm +XS8-normal-src/gpumon-24.1.0-40.xs8.src.rpm +XS8-normal-src/grub-2.06-4.0.2.xs8.src.rpm +XS8-normal-src/guest-templates-json-2.0.13-1.xs8.src.rpm +XS8-normal-src/host-installer-10.10.25-1.xs8.src.rpm +XS8-normal-src/host-upgrade-plugin-3.0.3-1.xs8.src.rpm +XS8-normal-src/hwloc-2.11.1_xen1.1.1-1.xs8.src.rpm +XS8-normal-src/intel-i40e-2.25.11-2.xs8.src.rpm +XS8-normal-src/intel-ice-1.15.5-2.xs8.src.rpm +XS8-normal-src/intel-igb-5.13.20-2.xs8.src.rpm +XS8-normal-src/intel-igc-5.10.214-3.xs8.src.rpm +XS8-normal-src/interface-rename-2.0.6-1.xs8.src.rpm +XS8-normal-src/ipxe-20121005-1.0.7.xs8.src.rpm +XS8-normal-src/ipxe-efi-20180514gite7f67d5-1.0.3.xs8.src.rpm +XS8-normal-src/irqbalance-1.0.7-15.xs8.src.rpm +XS8-normal-src/jemalloc-5.3.0-1.xs8.src.rpm +XS8-normal-src/kernel-4.19.19-8.0.38.xs8.src.rpm +XS8-normal-src/kernel-livepatch-1.0.4-1.xs8.src.rpm +XS8-normal-src/kexec-tools-2.0.15-20.xs8.src.rpm +XS8-normal-src/libbpf-1.4.0-3.xs8.src.rpm +XS8-normal-src/libcgroup-0.41-21.xs8.src.rpm +XS8-normal-src/libnbd-1.20.2-1.xs8.src.rpm +XS8-normal-src/linux-guest-loader-2.3.2-2.xs8.src.rpm +XS8-normal-src/livepatch-build-tools-20250121-1.xs8.src.rpm +XS8-normal-src/logrotate-3.8.6-21.xs8.src.rpm +XS8-normal-src/lvm2-2.02.180-18.xs8.src.rpm +XS8-normal-src/mcelog-196-3.xs8.src.rpm +XS8-normal-src/mellanox-mlnxen-5.9_0.5.5.0-2.xs8.src.rpm +XS8-normal-src/microsemi-smartpqi-2.1.30_031-1.xs8.src.rpm +XS8-normal-src/mpdecimal-2.5.1-1.xs8.src.rpm +XS8-normal-src/nagios-plugins-2.4.3-3.xs8.src.rpm +XS8-normal-src/ncurses-6.4-4.xs8.src.rpm +XS8-normal-src/net-snmp-5.7.2-52.xs8.src.rpm +XS8-normal-src/newt-0.52.23-4.xs8.src.rpm +XS8-normal-src/ninja-build-1.10.2-4.xs8.src.rpm +XS8-normal-src/nrpe-4.1.0-11.xs8.src.rpm +XS8-normal-src/ocaml-4.14.2-1.xs8.src.rpm +XS8-normal-src/ocaml-findlib-1.9.6-3.xs8.src.rpm +XS8-normal-src/opam-2.1.4-4.xs8.src.rpm +XS8-normal-src/openssh-7.4p1-23.3.xs8.src.rpm +XS8-normal-src/openssl-1.0.2k-26.xs8.src.rpm +XS8-normal-src/openvswitch-2.17.7-2.xs8.src.rpm +XS8-normal-src/plymouth-0.8.9-0.31.20140113.3.xs8.src.rpm +XS8-normal-src/psmisc-23.6-2.xs8.src.rpm +XS8-normal-src/pyproject-rpm-macros-1.8.0-4.xs8.src.rpm +XS8-normal-src/python-aiocontextvars-0.2.2-3.xs8.src.rpm +XS8-normal-src/python-charset-normalizer-2.1.0-4.xs8.src.rpm +XS8-normal-src/python-contextvars-2.4-3.xs8.src.rpm +XS8-normal-src/python-defusedxml-0.7.1-1.xs8.src.rpm +XS8-normal-src/python-deprecated-1.2.14-3.xs8.src.rpm +XS8-normal-src/python-fasteners-0.9.0-3.xs8.src.rpm +XS8-normal-src/python-hwinfo-0.1.11-1.xs8.src.rpm +XS8-normal-src/python-idna-3.3-4.xs8.src.rpm +XS8-normal-src/python-immutables-0.19-5.xs8.src.rpm +XS8-normal-src/python-netaddr-0.10.1-1.xs8.src.rpm +XS8-normal-src/python-pam-1.8.4-1.xs8.src.rpm +XS8-normal-src/python-prettytable-0.7.2-14.xs8.src.rpm +XS8-normal-src/python-psutil-5.9.1-2.xs8.src.rpm +XS8-normal-src/python-pyudev-0.21.0-2.xs8.src.rpm +XS8-normal-src/python-requests-2.28.1-4.xs8.src.rpm +XS8-normal-src/python-tqdm-4.50.2-1.xs8.src.rpm +XS8-normal-src/python-typing-extensions-3.7.4.3-4.xs8.src.rpm +XS8-normal-src/python-urllib3-1.26.20-3.xs8.src.rpm +XS8-normal-src/python-wheel-0.31.1-5.el7_7.src.rpm +XS8-normal-src/python-wrapt-1.14.0-4.xs8.src.rpm +XS8-normal-src/python-zstd-1.4.5.1-4.xs8.src.rpm +XS8-normal-src/qemu-4.2.1-5.2.12.xs8.src.rpm +XS8-normal-src/qemu-dp-7.0.0-17.xs8.src.rpm +XS8-normal-src/qlogic-fastlinq-8.74.0.2-1.xs8.src.rpm +XS8-normal-src/qlogic-qla2xxx-10.02.12.01_k-1.xs8.src.rpm +XS8-normal-src/rrd-client-lib-2.0.0-1.xs8.src.rpm +XS8-normal-src/rsync-3.4.1-1.xs8.src.rpm +XS8-normal-src/scapy-2.4.5-3.xs8.src.rpm +XS8-normal-src/sm-3.2.12-3.xs8.src.rpm +XS8-normal-src/smartmontools-7.4-2.xs8.src.rpm +XS8-normal-src/sm-core-libs-1.1.2-1.xs8.src.rpm +XS8-normal-src/sudo-1.9.15-4.xs8.src.rpm +XS8-normal-src/swtpm-0.7.3-8.xs8.src.rpm +XS8-normal-src/test-ring0-1.0.7-4.xs8.src.rpm +XS8-normal-src/varstored-1.2.0-2.xs8.src.rpm +XS8-normal-src/vcputune-2.0.2-1.xs8.src.rpm +XS8-normal-src/vendor-drivers-2.0.3-1.xs8.src.rpm +XS8-normal-src/vmss-1.2.1-1.xs8.src.rpm +XS8-normal-src/vncterm-10.2.1-2.xs8.src.rpm +XS8-normal-src/xapi-25.6.0-1.xs8.src.rpm +XS8-normal-src/xcp-python-libs-3.0.4-2.xs8.src.rpm +XS8-normal-src/xcp-python-libs-compat-2.3.5-6.xs8.src.rpm +XS8-normal-src/xen-4.17.5-9.xs8.src.rpm +XS8-normal-src/xencert-8.4.4-1.xs8.src.rpm +XS8-normal-src/xen-crashdump-analyser-2.6.1-1.xs8.src.rpm +XS8-normal-src/xen-livepatch-4-3.xs8.src.rpm +XS8-normal-src/xenserver-config-1.0.1-3.xs8.src.rpm +XS8-normal-src/xenserver-hwdata-20240411-1.xs8.src.rpm +XS8-normal-src/xenserver-nagios-plugins-1.0.6-1.xs8.src.rpm +XS8-normal-src/xenserver-plymouth-theme-1.4.0-2.xs8.src.rpm +XS8-normal-src/xenserver-release-8.4.0-15.xs8.src.rpm +XS8-normal-src/xenserver-status-report-2.0.11-1.xs8.src.rpm +XS8-normal-src/xha-25.0.0-1.xs8.src.rpm +XS8-normal-src/xsconsole-11.0.8-1.xs8.src.rpm +XS8-normal-src/xs-obsolete-packages-8-12.xs8.src.rpm +XS8-normal-src/xs-opam-repo-6.87.0-1.xs8.src.rpm diff --git a/scripts/repo_status/create_rebase_cards.py b/scripts/repo_status/create_rebase_cards.py index 6aada568..92e12783 100755 --- a/scripts/repo_status/create_rebase_cards.py +++ b/scripts/repo_status/create_rebase_cards.py @@ -7,7 +7,13 @@ from tabulate import tabulate import repoquery -from lib import collect_data_xcpng, collect_data_xs8, get_xs8_rpm_updates, read_package_status_metadata +from lib import ( + collect_data_xcpng, + collect_data_xs8, + get_xs8_rpm_updates, + read_package_status_metadata, + read_previous_packages, +) parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', action='count', default=0) @@ -17,6 +23,7 @@ logging.basicConfig(format='[%(levelname)s] %(message)s', level=loglevel) PACKAGE_STATUS = read_package_status_metadata() +PREVIOUS_PACKAGES = read_previous_packages('XS8-normal-src.txt') xcp_set = collect_data_xcpng() (xs8_srpms_set, xs8_rpms_sources_set) = collect_data_xs8() @@ -34,6 +41,9 @@ else: xs8_evr = xs8_srpms_evr or xs8_rpms_sources_evr xcp_evr = xcp_set.get(n) + if f'{n}-{xs8_evr}.xs8' in PREVIOUS_PACKAGES: + logging.info(f"ignoring previous package {n}") + continue xs8_update = srpm_updates.get(f'{n}-{xs8_evr}.xs8', '?') # if xcp_evr is not None and xcp_evr < xs8_evr: if xcp_evr is None: diff --git a/scripts/repo_status/lib.py b/scripts/repo_status/lib.py index 25317643..77d28f61 100755 --- a/scripts/repo_status/lib.py +++ b/scripts/repo_status/lib.py @@ -3,6 +3,7 @@ import csv import gzip import logging +import os import tempfile import xml.etree.ElementTree as ET from collections import namedtuple @@ -103,6 +104,10 @@ def read_package_status_metadata(): return {row[0]: PackageStatus(*row[1:]) for row in csvreader} +def read_previous_packages(path: str): + with open(path) as f: + return [repoquery.srpm_strip_src_rpm(os.path.basename(line.strip())) for line in f] + def get_xs8_rpm_updates(): NS = {'repo': 'http://linux.duke.edu/metadata/repo'} BASE_URL = 'http://repos/repos/XS8/normal/xs8p-normal' From feacd99711fff73e96d9b1d8f6d9ac4cf3a804de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Fri, 4 Jul 2025 16:11:53 +0200 Subject: [PATCH 16/16] ignore corosync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- scripts/repo_status/package_status.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/repo_status/package_status.csv b/scripts/repo_status/package_status.csv index 5782bb3a..b868d70f 100644 --- a/scripts/repo_status/package_status.csv +++ b/scripts/repo_status/package_status.csv @@ -5,6 +5,7 @@ bpftool;ignored;unused, why? capstone;ignored;unused, why? citrix-crypto-module;ignored;proprietary, patched FIPS openssl compiler-rt18;ignored;unused, why? +corosync;ignored;not supported dlm;ignored;previous dependency for corosync emu-manager;ignored;proprietary, replaced by xcp-emu-manager epel-release;ignored;unused, why?