pkgcore.bugzilla package

Submodules

Module contents

Typed client for the bugs.gentoo.org Bugzilla REST API.

The public surface is re-exported here; the implementation is split across submodules so that pkgcore.bugzilla.wire and friends can be imported without dragging in the HTTP transport.

>>> from pkgcore.bugzilla import Bugzilla, BugQuery, Component, FlagStatus
>>> bz = Bugzilla()
>>> query = (BugQuery.component(Component.STABILIZATION)
...          & BugQuery.unresolved()
...          & BugQuery.flag("sanity-check", FlagStatus.GRANTED))
>>> bugs = bz.search(query)
class pkgcore.bugzilla.AuthMode(*values)[source]

Bases: StrEnum

Where the api key is placed.

QUERY is the only mode bugs.gentoo.org honours today; HEADER exists for instances running Bugzilla 6.

HEADER = 'header'
QUERY = 'query'
class pkgcore.bugzilla.Bug(*, id: BugId = 0, summary: str = '', product: str = '', component: str = '', version: str = '', status: str = '', resolution: str = '', severity: str = '', priority: str = '', assigned_to: str = '', creator: str = '', cc: tuple[str, ...]=(), keywords: tuple[str, ...]=(), whiteboard: str = '', alias: tuple[str, ...]=(), tags: tuple[str, ...]=(), depends_on: tuple[~pkgcore.bugzilla.wire.BugId, ...]=(), blocks: tuple[~pkgcore.bugzilla.wire.BugId, ...]=(), see_also: tuple[str, ...]=(), groups: tuple[str, ...]=(), flags: tuple[~pkgcore.bugzilla.bug.Flag, ...]=(), deadline: date | None = None, creation_time: datetime = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), last_change_time: datetime = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), package_list: PackageList = <PackageList ''>, runtime_testing_required: RuntimeTesting = RuntimeTesting.UNSET)[source]

Bases: object

An immutable snapshot of a bug.

Anonymous requests get every email address truncated at the @, so assigned_to, creator and cc only hold full addresses when the client was given an api key.

alias: tuple[str, ...]
arches(known_arches: Container[str]) tuple[str, ...][source]

Arch names found in CC, tolerating truncated anonymous addresses

assigned_to: str
blocks: tuple[BugId, ...]
property category: BugCategory | None
cc: tuple[str, ...]
component: str
creation_time: datetime
creator: str
deadline: date | None
depends_on: tuple[BugId, ...]
flag(name: str) bool | None[source]

Tri-state status of a named flag, None when it isn’t set

flags: tuple[Flag, ...]
groups: tuple[str, ...]
id: BugId
keywords: tuple[str, ...]
last_change_time: datetime
package_list: PackageList
priority: str
product: str
resolution: str
property resolved: bool
runtime_testing_required: RuntimeTesting
property sanity_check: bool | None
property security: bool

Whether the bug lives in the security product.

Orthogonal to carrying the SECURITY keyword, which marks an ordinary bug as blocking a security one.

see_also: tuple[str, ...]
severity: str
status: str
summary: str
tags: tuple[str, ...]
property url: str
version: str
whiteboard: str
class pkgcore.bugzilla.BugCategory(*values)[source]

Bases: StrEnum

Gentoo arch team bug categories, valued by their Bugzilla component

KEYWORDREQ = 'Keywording'
STABLEREQ = 'Stabilization'
property component: Component
classmethod from_product_component(product: str, component: str) BugCategory | None[source]

Classify a bug, returning None if it’s neither category

property product: Product
property summary_suffix: str

The conventional trailing word of the bug summary

property verb: str

The verb used when describing the request

class pkgcore.bugzilla.BugChanges(id: BugId, last_change_time: datetime, changes: dict[str, ~pkgcore.bugzilla.bug.FieldChange]=<factory>, alias: tuple[str, ...]=())[source]

Bases: object

The result of updating a bug

alias: tuple[str, ...]
changes: dict[str, FieldChange]
id: BugId
last_change_time: datetime
class pkgcore.bugzilla.BugQuery(simple: tuple[tuple[str, tuple[str, ...]], ...] = (), charts: tuple[Criterion | ChartGroup, ...] = (), limit: int | None = None, offset: int | None = None, order: str | None = None)[source]

Bases: object

A search, built from named constructors and combined with &

classmethod any_of(*queries: BugQuery) BugQuery[source]

OR several chart-only queries together.

Simple parameters can’t take part in a chart group, so a query holding any is rejected rather than silently ANDed in.

classmethod assigned_to(*emails: str) BugQuery[source]
batches(base_length: int = 0, max_length: int = 6000) Iterator[BugQuery][source]

Split into sub-queries whose encoded parameters each fit the budget.

Only the largest splittable axis is divided, either the id simple parameter or a Criterion marked splittable; everything else is repeated in every batch. Sizing uses the encoded length rather than a count, so it adapts to long atoms instead of guessing.

classmethod category(*categories: BugCategory) BugQuery[source]

Restrict to keywordreqs and/or stablereqs

classmethod cc(*emails: str) BugQuery[source]
charts: tuple[Criterion | ChartGroup, ...]
classmethod component(*components: Component | str) BugQuery[source]
classmethod flag(name: str, *statuses: FlagStatus | str) BugQuery[source]

Match a flag by status.

Bugzilla can’t express “flag is absent”; those bugs have to be fetched and filtered client side.

classmethod ids(bugs: Iterable[BugId | int]) BugQuery[source]
classmethod keywords(*keywords: str) BugQuery[source]
limit: int | None
offset: int | None
order: str | None
classmethod package_list_any(packages: Iterable[object]) BugQuery[source]

Match bugs whose package list mentions any of these packages

paged(limit: int, offset: int = 0) BugQuery[source]

Return a copy with explicit paging.

Bugzilla rejects an offset without a limit, and treats limit=0 as unlimited while silently discarding the offset, so both are refused.

params() list[tuple[str, str]][source]

Render to ordered query parameters.

Ordered pairs rather than a mapping, since chart slots are positional and duplicate keys are meaningful.

classmethod product(*products: Product | str) BugQuery[source]
classmethod resolution(*resolutions: str) BugQuery[source]
simple: tuple[tuple[str, tuple[str, ...]], ...]
classmethod status(*statuses: Status | str) BugQuery[source]
classmethod unresolved() BugQuery[source]

Match open bugs, i.e. those with no resolution set.

Selecting on the open statuses instead returns the same set, since Bugzilla only leaves the resolution empty while the status is open, so this is the one spelling worth having.

classmethod without_tags(*tags: str) BugQuery[source]

Exclude bugs carrying any of these personal tags

class pkgcore.bugzilla.BugUpdate(*, status: Status | None = None, resolution: Resolution | None = None, dupe_of: BugId | None = None, summary: str | None = None, assigned_to: str | None = None, whiteboard: str | None = None, deadline: date | None = None, cc: ListChange[str] = ListChange(add=(), remove=(), replace=None), keywords: ListChange[str] = ListChange(add=(), remove=(), replace=None), blocks: ListChange[BugId] = ListChange(add=(), remove=(), replace=None), depends_on: ListChange[BugId] = ListChange(add=(), remove=(), replace=None), see_also: ListChange[str] = ListChange(add=(), remove=(), replace=None), groups: ListChange[str] = ListChange(add=(), remove=(), replace=None), flags: tuple[FlagChange, ...] = (), comment: NewComment | None = None, package_list: PackageList | None = None, runtime_testing_required: RuntimeTesting | None = None)[source]

Bases: object

A patch to apply to one or more bugs.

Every field defaults to leaving the bug alone. List valued fields only accept a ListChange, so the cc_add shape Bugzilla ignores is both a static and a runtime error.

assigned_to: str | None
blocks: ListChange[BugId]
cc: ListChange[str]
comment: NewComment | None
deadline: date | None
depends_on: ListChange[BugId]
dupe_of: BugId | None
flags: tuple[FlagChange, ...]
groups: ListChange[str]
keywords: ListChange[str]
classmethod obsoleted_by(bug: BugId | int, **kwargs: Any) BugUpdate[source]

Close as OBSOLETE, pointing at the bug that supersedes this one

package_list: PackageList | None
resolution: Resolution | None
classmethod resolve(resolution: Resolution = Resolution.FIXED, *, comment: str | None = None, **kwargs: Any) BugUpdate[source]
runtime_testing_required: RuntimeTesting | None
classmethod sanity_check(status: bool | None, *, comment: str | None = None, **kwargs: Any) BugUpdate[source]

Set the sanity-check flag, None clearing it

see_also: ListChange[str]
status: Status | None
summary: str | None
to_wire(ids: Sequence[BugId | int]) RawBugUpdate[source]

Render the payload.

The complete id list is always sent, since Bugzilla lets the body override the id in the request path rather than the other way around.

whiteboard: str | None
class pkgcore.bugzilla.Bugzilla(api_key: str | None = None, *, base_url: str = 'https://bugs.gentoo.org', timeout: float = 30.0, retries: int = 3, auth_mode: AuthMode = AuthMode.QUERY, retry_writes: bool = False, user_agent: str | None = None, opener: OpenerDirector | None = None, transport: Transport | None = None)[source]

Bases: object

A Bugzilla instance, defaulting to bugs.gentoo.org.

Without an api key the client is read only, and Bugzilla truncates every email address it returns at the @, so anything matching on addresses needs one.

user_agent names the calling tool; it is prepended to pkgcore’s own token rather than replacing it.

base_url
comments(bug: BugId | int, /) tuple[Comment, ...][source]

Every comment on a bug, oldest first

create(bug: NewBug, /) BugId[source]

File a bug and return its id.

Never retried, since a retry after a timeout files a duplicate.

get(bugs: BugId | int, /) Bug[source]
get(bugs: Iterable[BugId | int], /) dict[BugId, Bug]

Fetch one bug, or a mapping for several.

Raises:

BugzillaNotFound – when a single requested bug doesn’t exist

latest_comment(bug: BugId | int, /, *, creator: str | None = None) Comment | None[source]

The newest comment, optionally restricted to one author.

creator defaults to the authenticated account.

mark_own_comments_obsolete(bug: BugId | int, /) int[source]

Tag the authenticated user’s comments obsolete, returning the count.

Deliberately separate from update(), so a failed update doesn’t leave a bug with every comment obsoleted and no replacement.

Run a search, returning the wire dicts rather than Bug.

Use this for the narrow projections a full Bug doesn’t need; anything left out of fields is simply absent from the results.

resolve_dependencies(bugs: dict[BugId, Bug]) dict[BugId, Bug][source]

Fetch the transitive closure of everything bugs depends on.

Dependencies that can’t be fetched, because they were deleted or are behind a security group, are logged and dropped rather than looping.

search(query: BugQuery = BugQuery(simple=(), charts=(), limit=None, offset=None, order=None), /) dict[BugId, Bug][source]

Run a search, batching and paging as needed

tag_comments(comments: Iterable[CommentId | int], /, tags: ListChange[str]) None[source]

Add or remove tags on comments, one request per comment

update(bugs: BugId | int, /, update: BugUpdate) BugChanges[source]
update(bugs: Iterable[BugId | int], /, update: BugUpdate) tuple[BugChanges, ...]

Apply an update to one or more bugs.

The full id list always goes in the body, because Bugzilla lets the body override the id in the request path rather than the other way around.

whoami() User[source]

The account the api key belongs to, cached for the session

exception pkgcore.bugzilla.BugzillaError(msg, verbosity=None)[source]

Bases: PkgcoreUserException

Base for every failure raised by pkgcore.bugzilla

class pkgcore.bugzilla.Comment(id: CommentId, bug_id: BugId, count: int, text: str, creator: str, creation_time: datetime, is_private: bool = False, tags: tuple[str, ...] = ())[source]

Bases: object

A single comment on a bug

bug_id: BugId
count: int
creation_time: datetime
creator: str
id: CommentId
is_private: bool
property obsolete: bool
tags: tuple[str, ...]
text: str
class pkgcore.bugzilla.Component(*values)[source]

Bases: StrEnum

Components of Gentoo Linux, plus the security one that matters

CURRENT_PACKAGES = 'Current packages'
ECLASSES = 'Eclasses'
KEYWORDING = 'Keywording'
NEW_PACKAGES = 'New packages'
PROFILES = 'Profiles'
STABILIZATION = 'Stabilization'
VULNERABILITIES = 'Vulnerabilities'
class pkgcore.bugzilla.Criterion(field: str, op: ChartOp, values: tuple[str, ...] = (), negate: bool = False, splittable: bool = False)[source]

Bases: object

One boolean chart condition

field: str
negate: bool
op: ChartOp
render(slot: int) list[tuple[str, str]][source]
splittable: bool
values: tuple[str, ...]
with_values(values: Iterable[str]) Criterion[source]
class pkgcore.bugzilla.Flag(name: str, status: FlagStatus, id: FlagId = 0, type_id: FlagTypeId = 0, setter: str = '', requestee: str = '')[source]

Bases: object

A flag set on a bug

property granted: bool | None

Tri-state view: True for +, False for -, None otherwise

id: FlagId
name: str
requestee: str
setter: str
status: FlagStatus
type_id: FlagTypeId
class pkgcore.bugzilla.FlagChange(name: str, status: FlagStatus, requestee: str | None = None)[source]

Bases: object

Set or clear a flag

name: str
requestee: str | None
status: FlagStatus
to_wire() RawFlagChange[source]
class pkgcore.bugzilla.FlagStatus(*values)[source]

Bases: StrEnum

Status of a Bugzilla flag, CLEARED being write only

CLEARED = 'X'
DENIED = '-'
GRANTED = '+'
REQUESTED = '?'
class pkgcore.bugzilla.ListChange(add: tuple[T, ...] = (), remove: tuple[T, ...] = (), replace: tuple[T, ...] | None = None)[source]

Bases: Generic

An add/remove/set mutation of a list valued field

add: tuple[T, ...]
classmethod adding(*values: T) ListChange[T][source]
remove: tuple[T, ...]
classmethod removing(*values: T) ListChange[T][source]
replace: tuple[T, ...] | None
classmethod setting(*values: T) ListChange[T][source]

Replace the field wholesale, Bugzilla’s set

to_wire() RawListChange[source]
class pkgcore.bugzilla.NewBug(*, summary: str, description: str, component: Component | str, product: Product | str = Product.GENTOO_LINUX, version: str = 'unspecified', severity: Severity | str = Severity.NORMAL, assigned_to: str | None = None, cc: tuple[str, ...] = (), keywords: tuple[str, ...] = (), depends_on: tuple[BugId, ...] = (), blocks: tuple[BugId, ...] = (), see_also: tuple[str, ...] = (), deadline: date | None = None, package_list: PackageList | None = None, runtime_testing_required: RuntimeTesting | None = None)[source]

Bases: object

A bug to file

classmethod arch_request(category: BugCategory, package_list: PackageList, *, maintainers: Sequence[str] = (), cc_arches: bool = False, summary: str | None = None, description: str | None = None, **kwargs: Any) NewBug[source]

A keywordreq or stablereq, with the bgo conventions applied

assigned_to: str | None
blocks: tuple[BugId, ...]
cc: tuple[str, ...]
component: Component | str
deadline: date | None
depends_on: tuple[BugId, ...]
description: str
keywords: tuple[str, ...]
package_list: PackageList | None
classmethod package_mask(summary: str, description: str, *, rites: int, maintainers: Sequence[str] = (), today: date | None = None, **kwargs: Any) NewBug[source]

A last rites tracker, masked for rites days

product: Product | str
runtime_testing_required: RuntimeTesting | None
see_also: tuple[str, ...]
severity: Severity | str
summary: str
to_wire() RawNewBug[source]
version: str
class pkgcore.bugzilla.NewComment(body: str, is_private: bool = False)[source]

Bases: object

A comment to leave alongside an update

body: str
is_private: bool
to_wire() RawNewComment[source]
classmethod truncated(body: str, limit: int = 65535, **kwargs: Any) NewComment[source]

Build a comment, cutting an overlong body on a line boundary

class pkgcore.bugzilla.PackageList(text: str = '', *, bug_id: BugId | None = None)[source]

Bases: Simple

A lazily parsed view of a bug’s package list.

Parsing is deferred so fetching a bug with a malformed list never fails; only code that looks at the list does.

property atoms: tuple[atom, ...]
bug_id
classmethod build(entries: Iterable[tuple[atom, Iterable[str]]]) PackageList[source]

Construct a fresh list from atoms and their keywords

entries

Every line of the list, blanks and comments included

expand(suggest: Callable[[atom], Sequence[str]]) PackageList[source]

Resolve the * and ^ sentinels.

suggest returns the keywords a package should be requested for, in the order they should be written; returning nothing collapses the line to -.

Raises:

PackageListError – on ^ with nothing above it to copy

keywords_for(pkg: atom) tuple[str, ...][source]

Keywords requested for an atom, as written

text
class pkgcore.bugzilla.PackageListEntry(lineno: int, raw: str, pkg: atom | None, keywords: tuple[str, ...] = (), comment: str = '', eol: str = '')[source]

Bases: object

A single line of a package list

comment: str
eol: str
property is_blank: bool
keywords: tuple[str, ...]
lineno: int
pkg: atom | None
raw: str
with_keywords(keywords: Iterable[str]) PackageListEntry[source]

Return a copy with new keywords, keeping indentation and comment

class pkgcore.bugzilla.Product(*values)[source]

Bases: StrEnum

The bugs.gentoo.org products this module knows about

GENTOO_LINUX = 'Gentoo Linux'
GENTOO_SECURITY = 'Gentoo Security'
class pkgcore.bugzilla.Resolution(*values)[source]

Bases: StrEnum

Resolutions enabled on bugs.gentoo.org.

LATER and REMIND are legacy values still set on old bugs, listed so parsing round-trips rather than because anything should write them.

CANTFIX = 'CANTFIX'
DUPLICATE = 'DUPLICATE'
FIXED = 'FIXED'
INVALID = 'INVALID'
LATER = 'LATER'
NEEDINFO = 'NEEDINFO'
OBSOLETE = 'OBSOLETE'
PKGREMOVED = 'PKGREMOVED'
REMIND = 'REMIND'
TEST_REQUEST = 'TEST-REQUEST'
UPSTREAM = 'UPSTREAM'
WONTFIX = 'WONTFIX'
WORKSFORME = 'WORKSFORME'
class pkgcore.bugzilla.RuntimeTesting(*values)[source]

Bases: StrEnum

Values of cf_runtime_testing_required.

The field only exists on the Keywording and Stabilization components, and reads back as UNSET everywhere else.

MANUAL = 'Manual'
NO = 'No'
UNSET = '---'
YES = 'Yes'
class pkgcore.bugzilla.Severity(*values)[source]

Bases: StrEnum

Bug severities, QA being Gentoo specific

BLOCKER = 'blocker'
CRITICAL = 'critical'
ENHANCEMENT = 'enhancement'
MAJOR = 'major'
MINOR = 'minor'
NORMAL = 'normal'
QA = 'QA'
TRIVIAL = 'trivial'
class pkgcore.bugzilla.Status(*values)[source]

Bases: StrEnum

Bug workflow states, there is no NEW, ASSIGNED or CLOSED

CONFIRMED = 'CONFIRMED'
IN_PROGRESS = 'IN_PROGRESS'
RESOLVED = 'RESOLVED'
UNCONFIRMED = 'UNCONFIRMED'
VERIFIED = 'VERIFIED'
property is_open: bool
class pkgcore.bugzilla.User(id: int, name: str, real_name: str = '')[source]

Bases: object

The account an api key belongs to

id: int
name: str
real_name: str