cli/bugzilla: new module
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
fbe463ae39
commit
7301e8d1b6
8
.reuse/dep5
Normal file
8
.reuse/dep5
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Upstream-Name: find-work
|
||||||
|
Upstream-Contact: Anna <cyber@sysrq.in>
|
||||||
|
Source: https://find-work.sysrq.in/
|
||||||
|
|
||||||
|
Files: tests/cassettes/test_bugzilla/*
|
||||||
|
Copyright: Gentoo Authors
|
||||||
|
License: CC0-1.0
|
|
@ -28,5 +28,12 @@ You can use command aliases, for example:
|
||||||
|
|
||||||
find-work -I rep -r gentoo_ovl_guru out
|
find-work -I rep -r gentoo_ovl_guru out
|
||||||
|
|
||||||
|
To see which installed packages have open version bump requests filed on
|
||||||
|
Bugzilla, sorted by date last updated, run:
|
||||||
|
|
||||||
|
.. prompt:: bash
|
||||||
|
|
||||||
|
find-work -I bugzilla -t outdated
|
||||||
|
|
||||||
All data from APIs is cached for a day, so don't hesitate running the command
|
All data from APIs is cached for a day, so don't hesitate running the command
|
||||||
again and again!
|
again and again!
|
||||||
|
|
|
@ -12,11 +12,13 @@ interested in.
|
||||||
|
|
||||||
The following data sources are supported:
|
The following data sources are supported:
|
||||||
|
|
||||||
|
* `Gentoo Bugzilla`
|
||||||
* `Repology`_
|
* `Repology`_
|
||||||
|
|
||||||
|
.. _Gentoo Bugzilla: https://bugs.gentoo.org/
|
||||||
.. _Repology: https://repology.org/
|
.. _Repology: https://repology.org/
|
||||||
|
|
||||||
Support for other sources (like Bugzilla or Pkgcheck) is planned.
|
Support for other sources (like Soko or pkgcheck) is planned.
|
||||||
|
|
||||||
If you want to learn how to use find-work, check out the following resources:
|
If you want to learn how to use find-work, check out the following resources:
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ from datetime import date
|
||||||
import click
|
import click
|
||||||
from click_aliases import ClickAliasedGroup
|
from click_aliases import ClickAliasedGroup
|
||||||
|
|
||||||
|
import find_work.cli.bugzilla
|
||||||
import find_work.cli.repology
|
import find_work.cli.repology
|
||||||
from find_work.cli import Options
|
from find_work.cli import Options
|
||||||
from find_work.constants import VERSION
|
from find_work.constants import VERSION
|
||||||
|
@ -36,6 +37,31 @@ def cli(ctx: click.Context, quiet: bool, installed: bool) -> None:
|
||||||
options.cache_key += str(today).encode() + b"\0"
|
options.cache_key += str(today).encode() + b"\0"
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group(aliases=["bug", "b"], cls=ClickAliasedGroup)
|
||||||
|
@click.option("-c", "--component",
|
||||||
|
help="Component name on Bugzilla.")
|
||||||
|
@click.option("-p", "--product",
|
||||||
|
help="Product name on Bugzilla.")
|
||||||
|
@click.option("-t", "--time", is_flag=True,
|
||||||
|
help="Sort bugs by time last modified.")
|
||||||
|
@click.pass_obj
|
||||||
|
def bugzilla(options: Options, component: str | None, product: str | None,
|
||||||
|
time: bool) -> None:
|
||||||
|
""" Use Bugzilla to find work. """
|
||||||
|
|
||||||
|
options.bugzilla.chronological_sort = time
|
||||||
|
|
||||||
|
options.cache_key += b"bugzilla" + b"\0"
|
||||||
|
options.cache_key += b"time:" + b"1" if time else b"0" + b"\0"
|
||||||
|
|
||||||
|
if product:
|
||||||
|
options.bugzilla.product = product
|
||||||
|
options.cache_key += b"product:" + product.encode() + b"\0"
|
||||||
|
if component:
|
||||||
|
options.bugzilla.component = component
|
||||||
|
options.cache_key += b"component:" + component.encode() + b"\0"
|
||||||
|
|
||||||
|
|
||||||
@cli.group(aliases=["rep", "r"], cls=ClickAliasedGroup)
|
@cli.group(aliases=["rep", "r"], cls=ClickAliasedGroup)
|
||||||
@click.option("-r", "--repo", required=True,
|
@click.option("-r", "--repo", required=True,
|
||||||
help="Repository name on Repology.")
|
help="Repository name on Repology.")
|
||||||
|
@ -49,4 +75,6 @@ def repology(options: Options, repo: str) -> None:
|
||||||
options.cache_key += repo.encode() + b"\0"
|
options.cache_key += repo.encode() + b"\0"
|
||||||
|
|
||||||
|
|
||||||
|
bugzilla.add_command(find_work.cli.bugzilla.outdated, aliases=["out", "o"])
|
||||||
|
|
||||||
repology.add_command(find_work.cli.repology.outdated, aliases=["out", "o"])
|
repology.add_command(find_work.cli.repology.outdated, aliases=["out", "o"])
|
||||||
|
|
|
@ -54,10 +54,24 @@ class ProgressDots:
|
||||||
class RepologyOptions:
|
class RepologyOptions:
|
||||||
""" Repology subcommand options. """
|
""" Repology subcommand options. """
|
||||||
|
|
||||||
# Repository name
|
# Repository name.
|
||||||
repo: str = ""
|
repo: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BugzillaOptions:
|
||||||
|
""" Bugzilla subcommand options. """
|
||||||
|
|
||||||
|
# Product name.
|
||||||
|
product: str = ""
|
||||||
|
|
||||||
|
# Component name.
|
||||||
|
component: str = ""
|
||||||
|
|
||||||
|
# Sort by date last modified or by ID.
|
||||||
|
chronological_sort: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Options:
|
class Options:
|
||||||
""" Global options. """
|
""" Global options. """
|
||||||
|
@ -71,11 +85,12 @@ class Options:
|
||||||
# Filter installed packages only
|
# Filter installed packages only
|
||||||
only_installed: bool = False
|
only_installed: bool = False
|
||||||
|
|
||||||
# String used for creating cache key
|
# Byte string used for creating cache key.
|
||||||
cache_key: bytes = b""
|
cache_key: bytes = b""
|
||||||
|
|
||||||
# Repology subcommand options
|
# Subcommand options.
|
||||||
repology: RepologyOptions = field(default_factory=RepologyOptions)
|
repology: RepologyOptions = field(default_factory=RepologyOptions)
|
||||||
|
bugzilla: BugzillaOptions = field(default_factory=BugzillaOptions)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def echo(*args: Any, **kwargs: Any) -> None:
|
def echo(*args: Any, **kwargs: Any) -> None:
|
||||||
|
|
168
find_work/cli/bugzilla.py
Normal file
168
find_work/cli/bugzilla.py
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
# SPDX-License-Identifier: WTFPL
|
||||||
|
# SPDX-FileCopyrightText: 2024 Anna <cyber@sysrq.in>
|
||||||
|
# No warranty
|
||||||
|
|
||||||
|
"""
|
||||||
|
CLI subcommands for everything Bugzilla.
|
||||||
|
|
||||||
|
This Python module also defines some regular expressions.
|
||||||
|
|
||||||
|
``pkg_re`` matches package name and version from bug summaries:
|
||||||
|
|
||||||
|
>>> ant_match = pkg_re.search(">=dev-java/ant-1.10.14: version bump - needed for jdk:21")
|
||||||
|
>>> (ant_match.group("package"), ant_match.group("version"))
|
||||||
|
('dev-java/ant', '1.10.14')
|
||||||
|
>>> libjxl_match = pkg_re.search("media-libs/libjxl: version bump")
|
||||||
|
>>> (libjxl_match.group("package"), libjxl_match.group("version"))
|
||||||
|
('media-libs/libjxl', None)
|
||||||
|
>>> tricky_match = pkg_re.search("app-foo/bar-2-baz-4.0: version bump")
|
||||||
|
>>> (tricky_match.group("package"), tricky_match.group("version"))
|
||||||
|
('app-foo/bar-2-baz', '4.0')
|
||||||
|
>>> pkg_re.search("Please bump Firefox") is None
|
||||||
|
True
|
||||||
|
|
||||||
|
``isodate_re`` matches ISO 8601 time/date strings:
|
||||||
|
|
||||||
|
>>> isodate_re.fullmatch("2024") is None
|
||||||
|
True
|
||||||
|
>>> isodate_re.fullmatch("20090916T09:04:18") is None
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import warnings
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import Any
|
||||||
|
from xmlrpc.client import DateTime
|
||||||
|
|
||||||
|
import click
|
||||||
|
import gentoopm
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
from find_work.cli import Options, ProgressDots
|
||||||
|
from find_work.constants import BUGZILLA_URL
|
||||||
|
from find_work.types import BugView
|
||||||
|
from find_work.utils import (
|
||||||
|
requests_session,
|
||||||
|
read_json_cache,
|
||||||
|
write_json_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# Disable annoying warning shown to LibreSSL users
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
import bugzilla
|
||||||
|
from bugzilla.bug import Bug
|
||||||
|
|
||||||
|
# `category/package` matching according to PMS, and arbitrary version
|
||||||
|
pkg_re = re.compile(r"""(?P<package>
|
||||||
|
[\w][-+.\w]* # category
|
||||||
|
/ # single slash
|
||||||
|
[\w][+\w]* # package name before first '-'
|
||||||
|
(-[+\w]*(?=-))* # rest of package name
|
||||||
|
)
|
||||||
|
(
|
||||||
|
- # single hyphen
|
||||||
|
(?P<version>
|
||||||
|
\d+[.\w]* # arbitrary version
|
||||||
|
)
|
||||||
|
)?""",
|
||||||
|
re.VERBOSE)
|
||||||
|
|
||||||
|
isodate_re = re.compile(r"\d{4}\d{2}\d{2}T\d{2}:\d{2}:\d{2}")
|
||||||
|
|
||||||
|
|
||||||
|
class BugEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o: Any) -> Any:
|
||||||
|
if isinstance(o, DateTime):
|
||||||
|
return o.value
|
||||||
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
|
def as_datetime(obj: dict) -> dict:
|
||||||
|
result: dict = {}
|
||||||
|
for key, value in obj.items():
|
||||||
|
# FIXME: every matching string will be converted to DateTime
|
||||||
|
if isinstance(value, str) and isodate_re.fullmatch(value):
|
||||||
|
result[key] = DateTime(value)
|
||||||
|
continue
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _bugs_from_json(data: list[dict]) -> list[Bug]:
|
||||||
|
with requests_session() as session:
|
||||||
|
bz = bugzilla.Bugzilla(BUGZILLA_URL, requests_session=session)
|
||||||
|
return [Bug(bz, dict=bug) for bug in data]
|
||||||
|
|
||||||
|
|
||||||
|
def _bugs_to_json(data: Iterable[Bug]) -> list[dict]:
|
||||||
|
return [bug.get_raw_data() for bug in data]
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_bump_requests(options: Options) -> list[Bug]:
|
||||||
|
with requests_session() as session:
|
||||||
|
bz = bugzilla.Bugzilla(BUGZILLA_URL, requests_session=session)
|
||||||
|
query = bz.build_query(
|
||||||
|
product=options.bugzilla.product or None,
|
||||||
|
component=options.bugzilla.component or None,
|
||||||
|
short_desc="version bump",
|
||||||
|
)
|
||||||
|
query["resolution"] = "---"
|
||||||
|
if options.bugzilla.chronological_sort:
|
||||||
|
query["order"] = "changeddate DESC"
|
||||||
|
else:
|
||||||
|
query["order"] = "bug_id DESC"
|
||||||
|
return bz.query(query)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_bump_requests(data: Iterable[Bug],
|
||||||
|
options: Options) -> list[BugView]:
|
||||||
|
if options.only_installed:
|
||||||
|
pm = gentoopm.get_package_manager()
|
||||||
|
|
||||||
|
result: list[BugView] = []
|
||||||
|
for bug in data:
|
||||||
|
if options.only_installed:
|
||||||
|
if (match := pkg_re.search(bug.summary)) is None:
|
||||||
|
continue
|
||||||
|
if match.group("package") not in pm.installed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
date = time.strftime("%F", bug.last_change_time.timetuple())
|
||||||
|
item = BugView(bug.id, date, bug.assigned_to, bug.summary)
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.pass_obj
|
||||||
|
def outdated(options: Options) -> None:
|
||||||
|
""" Find packages with version bump requests on Bugzilla. """
|
||||||
|
options.cache_key += b"outdated" + b"\0"
|
||||||
|
dots = ProgressDots(options.verbose)
|
||||||
|
|
||||||
|
options.vecho("Checking for cached data", nl=False, err=True)
|
||||||
|
with dots():
|
||||||
|
cached_data = read_json_cache(options.cache_key,
|
||||||
|
object_hook=as_datetime)
|
||||||
|
if cached_data is not None:
|
||||||
|
options.vecho("Loading cached data", nl=False, err=True)
|
||||||
|
with dots():
|
||||||
|
data = _bugs_from_json(cached_data)
|
||||||
|
else:
|
||||||
|
options.vecho("Fetching data from Bugzilla API", nl=False, err=True)
|
||||||
|
with dots():
|
||||||
|
data = _fetch_bump_requests(options)
|
||||||
|
options.vecho("Caching data", nl=False, err=True)
|
||||||
|
with dots():
|
||||||
|
json_data = _bugs_to_json(data)
|
||||||
|
write_json_cache(json_data, options.cache_key, cls=BugEncoder)
|
||||||
|
|
||||||
|
bumps = _collect_bump_requests(data, options)
|
||||||
|
if len(bumps) == 0:
|
||||||
|
options.secho("Congrats! You have nothing to do!", fg="green")
|
||||||
|
else:
|
||||||
|
options.echo(tabulate(bumps, tablefmt="plain")) # type: ignore
|
|
@ -104,16 +104,15 @@ async def _outdated(options: Options) -> None:
|
||||||
write_json_cache(json_data, options.cache_key)
|
write_json_cache(json_data, options.cache_key)
|
||||||
|
|
||||||
outdated_set = _collect_version_bumps(data.values(), options)
|
outdated_set = _collect_version_bumps(data.values(), options)
|
||||||
if len(outdated_set) == 0:
|
|
||||||
options.secho("Congrats! You have nothing to do!", fg="green")
|
|
||||||
return
|
|
||||||
|
|
||||||
for bump in outdated_set:
|
for bump in outdated_set:
|
||||||
options.echo(bump.atom + " ", nl=False)
|
options.echo(bump.atom + " ", nl=False)
|
||||||
options.secho(bump.old_version, fg="red", nl=False)
|
options.secho(bump.old_version, fg="red", nl=False)
|
||||||
options.echo(" → ", nl=False)
|
options.echo(" → ", nl=False)
|
||||||
options.secho(bump.new_version, fg="green")
|
options.secho(bump.new_version, fg="green")
|
||||||
|
|
||||||
|
if len(outdated_set) == 0:
|
||||||
|
options.secho("Congrats! You have nothing to do!", fg="green")
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
|
|
|
@ -13,5 +13,8 @@ VERSION = "0.2.0"
|
||||||
# Application homepage.
|
# Application homepage.
|
||||||
HOMEPAGE = "https://find-work.sysrq.in"
|
HOMEPAGE = "https://find-work.sysrq.in"
|
||||||
|
|
||||||
# Application's User-agent header
|
# Application's User-agent header.
|
||||||
USER_AGENT = f"Mozilla/5.0 (compatible; {PACKAGE}/{VERSION}; +{HOMEPAGE})"
|
USER_AGENT = f"Mozilla/5.0 (compatible; {PACKAGE}/{VERSION}; +{HOMEPAGE})"
|
||||||
|
|
||||||
|
# Gentoo Bugzilla location.
|
||||||
|
BUGZILLA_URL = "https://bugs.gentoo.org"
|
||||||
|
|
|
@ -16,3 +16,13 @@ class VersionBump:
|
||||||
atom: str
|
atom: str
|
||||||
old_version: str = field(compare=False)
|
old_version: str = field(compare=False)
|
||||||
new_version: str = field(compare=False)
|
new_version: str = field(compare=False)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, order=True)
|
||||||
|
class BugView:
|
||||||
|
""" Bug listing item representation. """
|
||||||
|
|
||||||
|
bug_id: int
|
||||||
|
last_change_date: str = field(compare=False)
|
||||||
|
assigned_to: str = field(compare=False)
|
||||||
|
summary: str = field(compare=False)
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import warnings
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -16,11 +17,16 @@ import aiohttp
|
||||||
|
|
||||||
from find_work.constants import PACKAGE, USER_AGENT
|
from find_work.constants import PACKAGE, USER_AGENT
|
||||||
|
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# Disable annoying warning shown to LibreSSL users
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def aiohttp_session() -> AsyncGenerator[aiohttp.ClientSession, None]:
|
async def aiohttp_session() -> AsyncGenerator[aiohttp.ClientSession, None]:
|
||||||
"""
|
"""
|
||||||
Construct an :py:class:`aiohttp.ClientSession` object.
|
Construct an :py:class:`aiohttp.ClientSession` object with out settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
headers = {"user-agent": USER_AGENT}
|
headers = {"user-agent": USER_AGENT}
|
||||||
|
@ -33,18 +39,28 @@ async def aiohttp_session() -> AsyncGenerator[aiohttp.ClientSession, None]:
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def requests_session() -> requests.Session:
|
||||||
|
"""
|
||||||
|
Construct an :py:class:`requests.Session` object with out settings.
|
||||||
|
"""
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers["user-agent"] = USER_AGENT
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
def _get_cache_path(cache_key: bytes) -> Path:
|
def _get_cache_path(cache_key: bytes) -> Path:
|
||||||
hexdigest = hashlib.sha256(cache_key).hexdigest()
|
hexdigest = hashlib.sha256(cache_key).hexdigest()
|
||||||
file = Path(tempfile.gettempdir()) / PACKAGE / hexdigest
|
file = Path(tempfile.gettempdir()) / PACKAGE / hexdigest
|
||||||
return file.with_suffix(".json")
|
return file.with_suffix(".json")
|
||||||
|
|
||||||
|
|
||||||
def write_json_cache(data: Any, cache_key: bytes) -> None:
|
def write_json_cache(data: Any, cache_key: bytes, **kwargs: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Write a JSON cache file in a temporary directory.
|
Write a JSON cache file in a temporary directory. Keyword arguments are
|
||||||
|
passed to :py:function:`json.dump` as is.
|
||||||
|
|
||||||
:param data: data to serialize
|
:param data: data to serialize
|
||||||
:param cache_key: hash object to use as a key
|
:param cache_key: byte string to use as a key
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cache = _get_cache_path(cache_key)
|
cache = _get_cache_path(cache_key)
|
||||||
|
@ -55,16 +71,17 @@ def write_json_cache(data: Any, cache_key: bytes) -> None:
|
||||||
|
|
||||||
with open(cache, "w") as file:
|
with open(cache, "w") as file:
|
||||||
try:
|
try:
|
||||||
json.dump(data, file)
|
json.dump(data, file, **kwargs)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def read_json_cache(cache_key: bytes) -> Any | None:
|
def read_json_cache(cache_key: bytes, **kwargs: Any) -> Any | None:
|
||||||
"""
|
"""
|
||||||
Read a JSON cache file stored in a temporary directory.
|
Read a JSON cache file stored in a temporary directory. Keyword arguments
|
||||||
|
are passed to :py:function:`json.load` as is.
|
||||||
|
|
||||||
:param cache_key: hash object to use as a key
|
:param cache_key: byte string to use as a key
|
||||||
:returns: decoded data or ``None``
|
:returns: decoded data or ``None``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -73,4 +90,4 @@ def read_json_cache(cache_key: bytes) -> Any | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with open(cache) as file:
|
with open(cache) as file:
|
||||||
return json.load(file)
|
return json.load(file, **kwargs)
|
||||||
|
|
|
@ -40,8 +40,38 @@ The modules for
|
||||||
are as follows:
|
are as follows:
|
||||||
.Bl -tag -width repology
|
.Bl -tag -width repology
|
||||||
.It Xo
|
.It Xo
|
||||||
|
.Cm bugzilla
|
||||||
|
.Ol Fl t
|
||||||
|
.Op Fl c Ar name
|
||||||
|
.Op Fl p Ar name
|
||||||
|
.Ar command
|
||||||
|
.Xc
|
||||||
|
.Dl Pq alias: Cm bug , Cm b
|
||||||
|
.Pp
|
||||||
|
This module uses Gentoo Bugzilla to find work.
|
||||||
|
.Pp
|
||||||
|
.Ar command
|
||||||
|
can be one of the following:
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Ic outdated Pq alias: Ic out , Ic o
|
||||||
|
Find open version bump requests.
|
||||||
|
.El
|
||||||
|
.Pp
|
||||||
|
The options for
|
||||||
|
.Cm find-work bugzilla
|
||||||
|
are as follows:
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Fl c Ar name , Fl -component Ar name
|
||||||
|
Component name on Bugzilla.
|
||||||
|
.It Fl p Ar name , Fl -product Ar name
|
||||||
|
Product name on Bugzilla.
|
||||||
|
.It Fl t , Fl -time
|
||||||
|
Sort by time last modified (most recently modified first).
|
||||||
|
.El
|
||||||
|
.
|
||||||
|
.It Xo
|
||||||
.Cm repology
|
.Cm repology
|
||||||
.Op Fl r Ar repo
|
.Fl r Ar repo
|
||||||
.Ar command
|
.Ar command
|
||||||
.Xc
|
.Xc
|
||||||
.Dl Pq alias: Cm rep , Cm r
|
.Dl Pq alias: Cm rep , Cm r
|
||||||
|
@ -60,7 +90,7 @@ The options for
|
||||||
are as follows:
|
are as follows:
|
||||||
.Bl -tag -width Ds
|
.Bl -tag -width Ds
|
||||||
.It Fl r Ar repo , Fl -repo Ar repo
|
.It Fl r Ar repo , Fl -repo Ar repo
|
||||||
Repository name on repology.
|
Repository name on Repology.
|
||||||
This option is required.
|
This option is required.
|
||||||
Some examples for Gentoo include:
|
Some examples for Gentoo include:
|
||||||
.Bl -bullet -compact -width 1n
|
.Bl -bullet -compact -width 1n
|
||||||
|
@ -75,7 +105,7 @@ Some examples for Gentoo include:
|
||||||
.El
|
.El
|
||||||
.El
|
.El
|
||||||
.Sh ENVIRONMENT
|
.Sh ENVIRONMENT
|
||||||
.Bl -tag -width NOCOLOR
|
.Bl -tag -width NO_COLOR
|
||||||
.It Ev NO_COLOR
|
.It Ev NO_COLOR
|
||||||
If defined, disable all color output.
|
If defined, disable all color output.
|
||||||
.El
|
.El
|
||||||
|
|
|
@ -21,8 +21,11 @@ dependencies = [
|
||||||
"click-aliases",
|
"click-aliases",
|
||||||
"gentoopm<2",
|
"gentoopm<2",
|
||||||
"pydantic<3,>=2",
|
"pydantic<3,>=2",
|
||||||
|
"python-bugzilla",
|
||||||
"repology-client<2,>=0.0.2",
|
"repology-client<2,>=0.0.2",
|
||||||
|
"requests<3,>=2",
|
||||||
"sortedcontainers",
|
"sortedcontainers",
|
||||||
|
"tabulate",
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
|
@ -46,6 +49,7 @@ docs = [
|
||||||
test = [
|
test = [
|
||||||
"pkgcore",
|
"pkgcore",
|
||||||
"pytest",
|
"pytest",
|
||||||
|
"pytest-recording",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
@ -70,6 +74,9 @@ include = [
|
||||||
"/tests",
|
"/tests",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "--doctest-modules --block-network"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
disallow_untyped_defs = true
|
disallow_untyped_defs = true
|
||||||
no_implicit_optional = true
|
no_implicit_optional = true
|
||||||
|
@ -84,6 +91,8 @@ check_untyped_defs = true
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = [
|
module = [
|
||||||
|
"bugzilla",
|
||||||
|
"bugzilla.*",
|
||||||
"click_aliases",
|
"click_aliases",
|
||||||
"gentoopm",
|
"gentoopm",
|
||||||
"gentoopm.*",
|
"gentoopm.*",
|
||||||
|
|
108
tests/cassettes/test_bugzilla/test_bugs_json_roundtrip.yaml
Normal file
108
tests/cassettes/test_bugzilla/test_bugs_json_roundtrip.yaml
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: null
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
User-Agent:
|
||||||
|
- python-requests/2.31.0
|
||||||
|
method: HEAD
|
||||||
|
uri: https://bugs.gentoo.org/xmlrpc.cgi
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: ''
|
||||||
|
headers:
|
||||||
|
Connection:
|
||||||
|
- Keep-Alive
|
||||||
|
Content-Type:
|
||||||
|
- text/plain; charset=utf-8
|
||||||
|
Date:
|
||||||
|
- Wed, 10 Jan 2024 02:02:40 GMT
|
||||||
|
Keep-Alive:
|
||||||
|
- timeout=15, max=100
|
||||||
|
Server:
|
||||||
|
- Apache
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
- request:
|
||||||
|
body: '<?xml version=''1.0''?>
|
||||||
|
|
||||||
|
<methodCall>
|
||||||
|
|
||||||
|
<methodName>Bugzilla.version</methodName>
|
||||||
|
|
||||||
|
<params>
|
||||||
|
|
||||||
|
<param>
|
||||||
|
|
||||||
|
<value><struct>
|
||||||
|
|
||||||
|
</struct></value>
|
||||||
|
|
||||||
|
</param>
|
||||||
|
|
||||||
|
</params>
|
||||||
|
|
||||||
|
</methodCall>
|
||||||
|
|
||||||
|
'
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '161'
|
||||||
|
Content-Type:
|
||||||
|
- text/xml
|
||||||
|
User-Agent:
|
||||||
|
- python-bugzilla/3.2.0
|
||||||
|
method: POST
|
||||||
|
uri: https://bugs.gentoo.org/xmlrpc.cgi
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: <?xml version="1.0" encoding="UTF-8"?><methodResponse><params><param><value><struct><member><name>version</name><value><string>5.0.6</string></value></member></struct></value></param></params></methodResponse>
|
||||||
|
headers:
|
||||||
|
Connection:
|
||||||
|
- Keep-Alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- text/xml
|
||||||
|
Content-security-policy:
|
||||||
|
- frame-ancestors 'self'
|
||||||
|
Date:
|
||||||
|
- Wed, 10 Jan 2024 02:02:41 GMT
|
||||||
|
ETag:
|
||||||
|
- 7sMmJjLeC0KDNcgYXJQLEw
|
||||||
|
Keep-Alive:
|
||||||
|
- timeout=15, max=100
|
||||||
|
SOAPServer:
|
||||||
|
- SOAP::Lite/Perl/1.27
|
||||||
|
Server:
|
||||||
|
- Apache
|
||||||
|
Set-Cookie:
|
||||||
|
- Bugzilla_login_request_cookie=0jZsUHjVEQ; path=/; secure; HttpOnly
|
||||||
|
Strict-transport-security:
|
||||||
|
- max-age=15768000; includeSubDomains
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
Vary:
|
||||||
|
- Accept-Encoding
|
||||||
|
X-content-type-options:
|
||||||
|
- nosniff
|
||||||
|
X-frame-options:
|
||||||
|
- SAMEORIGIN
|
||||||
|
X-xss-protection:
|
||||||
|
- 1; mode=block
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
1
tests/data/bug74072.json
Normal file
1
tests/data/bug74072.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"id": 74072, "cc_detail": [{"name": "cantel", "id": 68480, "email": "cantel", "real_name": "Alex"}, {"name": "Captainsifff", "id": 32737, "email": "Captainsifff", "real_name": "Captain Sifff"}], "is_confirmed": true, "url": "", "cf_runtime_testing_required": "---", "flags": [], "is_open": false, "blocks": [], "op_sys": "Linux", "keywords": [], "component": "[OLD] Unspecified", "platform": "All", "groups": [], "depends_on": [], "qa_contact": "", "last_change_time": "20090916T09:04:18", "assigned_to": "bug-wranglers", "classification": "Unclassified", "priority": "High", "creator_detail": {"id": 17226, "name": "Augury", "real_name": "augury@vampares.org", "email": "Augury"}, "assigned_to_detail": {"name": "bug-wranglers", "id": 921, "email": "bug-wranglers", "real_name": "Gentoo Linux bug wranglers"}, "is_creator_accessible": true, "see_also": [], "cf_stabilisation_atoms": "", "alias": [], "version": "unspecified", "summary": "ld errors", "whiteboard": "", "severity": "trivial", "resolution": "WONTFIX", "is_cc_accessible": true, "creator": "Augury", "creation_time": "20041211T01:09:12", "product": "Gentoo Linux", "cc": ["cantel", "Captainsifff"], "status": "RESOLVED", "target_milestone": "---"}]
|
3
tests/data/bug74072.json.license
Normal file
3
tests/data/bug74072.json.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
SPDX-FileCopyrightText: 2024 Gentoo Authors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC0-1.0
|
20
tests/test_bugzilla.py
Normal file
20
tests/test_bugzilla.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# SPDX-License-Identifier: WTFPL
|
||||||
|
# SPDX-FileCopyrightText: 2024 Anna <cyber@sysrq.in>
|
||||||
|
# No warranty
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from find_work.cli.bugzilla import (
|
||||||
|
_bugs_from_json,
|
||||||
|
_bugs_to_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr
|
||||||
|
def test_bugs_json_roundtrip():
|
||||||
|
with open(Path(__file__).parent / "data" / "bug74072.json") as file:
|
||||||
|
data: list[dict] = json.load(file)
|
||||||
|
assert data == _bugs_to_json(_bugs_from_json(data))
|
|
@ -5,9 +5,9 @@
|
||||||
from sortedcontainers import SortedSet
|
from sortedcontainers import SortedSet
|
||||||
from repology_client.types import Package
|
from repology_client.types import Package
|
||||||
|
|
||||||
|
from find_work.types import VersionBump
|
||||||
from find_work.cli import Options
|
from find_work.cli import Options
|
||||||
from find_work.cli.repology import (
|
from find_work.cli.repology import (
|
||||||
VersionBump,
|
|
||||||
_collect_version_bumps,
|
_collect_version_bumps,
|
||||||
_projects_from_json,
|
_projects_from_json,
|
||||||
_projects_to_json,
|
_projects_to_json,
|
||||||
|
@ -15,21 +15,24 @@ from find_work.cli.repology import (
|
||||||
|
|
||||||
|
|
||||||
def test_projects_json_roundtrip():
|
def test_projects_json_roundtrip():
|
||||||
pkg1 = Package(
|
data = {
|
||||||
repo="gentoo",
|
"firefox": {
|
||||||
visiblename="www-client/firefox",
|
Package(
|
||||||
version="9999",
|
repo="gentoo",
|
||||||
status="test",
|
visiblename="www-client/firefox",
|
||||||
licenses=frozenset(["GPL-2", "LGPL-2.1", "MPL-2.0"]),
|
version="9999",
|
||||||
)
|
status="test",
|
||||||
pkg2 = Package(
|
licenses=frozenset(["GPL-2", "LGPL-2.1", "MPL-2.0"]),
|
||||||
repo="gentoo",
|
),
|
||||||
visiblename="www-client/firefox-bin",
|
Package(
|
||||||
version="9999",
|
repo="gentoo",
|
||||||
status="test",
|
visiblename="www-client/firefox-bin",
|
||||||
licenses=frozenset(["GPL-2", "LGPL-2.1", "MPL-2.0"]),
|
version="9999",
|
||||||
)
|
status="test",
|
||||||
data = {"firefox": {pkg1, pkg2}}
|
licenses=frozenset(["GPL-2", "LGPL-2.1", "MPL-2.0"]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
assert data == _projects_from_json(_projects_to_json(data))
|
assert data == _projects_from_json(_projects_to_json(data))
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,7 +61,7 @@ def test_collect_version_bumps():
|
||||||
version="1",
|
version="1",
|
||||||
status="outdated",
|
status="outdated",
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expected = SortedSet([VersionBump("dev-util/examplepkg", "1", "2")])
|
expected = SortedSet([VersionBump("dev-util/examplepkg", "1", "2")])
|
||||||
|
@ -96,7 +99,7 @@ def test_collect_version_bumps_multi_versions():
|
||||||
version="1",
|
version="1",
|
||||||
status="outdated",
|
status="outdated",
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expected = SortedSet([VersionBump("dev-util/examplepkg", "1", "2")])
|
expected = SortedSet([VersionBump("dev-util/examplepkg", "1", "2")])
|
||||||
|
@ -141,7 +144,7 @@ def test_collect_version_bumps_multi_names():
|
||||||
version="1",
|
version="1",
|
||||||
status="outdated",
|
status="outdated",
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expected = SortedSet([
|
expected = SortedSet([
|
||||||
|
|
Loading…
Reference in New Issue
Block a user