*/*: make better use of Pydantic models
This is a big messy commit that extends use of Pydantic for serialization and deserialization.
This commit is contained in:
parent
c632100837
commit
ae896790e3
|
@ -10,8 +10,15 @@ Release Notes
|
|||
|
||||
* **New**: Filter oudated packages by version part (:bug:`4`).
|
||||
|
||||
* Use Pydantic models to load and serialize caches. This could have better
|
||||
perfomance and correctness but also introduce new bugs.
|
||||
|
||||
*Modules changelog:*
|
||||
|
||||
* **bugzilla**:
|
||||
|
||||
* Switch to REST API from XMLRPC.
|
||||
|
||||
* **pgo**:
|
||||
|
||||
* **outdated**:
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
""" Implementation of caching functionality. """
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any, SupportsBytes
|
||||
|
@ -110,15 +109,14 @@ def _get_cache_path(cache_key: SupportsBytes) -> Path:
|
|||
return file.with_suffix(".json")
|
||||
|
||||
|
||||
def write_json_cache(data: Any, cache_key: SupportsBytes, *, raw: bool = False,
|
||||
**kwargs: Any) -> None:
|
||||
def write_raw_json_cache(data: SupportsBytes, cache_key: SupportsBytes) -> None:
|
||||
"""
|
||||
Write a JSON cache file in a temporary directory. Keyword arguments are
|
||||
passed to :py:function:`json.dump` as is.
|
||||
Write a JSON cache file in a temporary directory.
|
||||
|
||||
:param data: data to serialize
|
||||
This function silently fails on OS errors.
|
||||
|
||||
:param data: raw JSON
|
||||
:param cache_key: cache key object
|
||||
:param raw: skip encoding and write raw data instead
|
||||
"""
|
||||
|
||||
cache = _get_cache_path(cache_key)
|
||||
|
@ -129,31 +127,23 @@ def write_json_cache(data: Any, cache_key: SupportsBytes, *, raw: bool = False,
|
|||
|
||||
with open(cache, "wb") as file:
|
||||
try:
|
||||
if raw:
|
||||
cache.write_bytes(bytes(data))
|
||||
return
|
||||
json.dump(data, file, **kwargs)
|
||||
file.write(bytes(data))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def read_json_cache(cache_key: SupportsBytes, *, raw: bool = False,
|
||||
**kwargs: Any) -> Any | None:
|
||||
def read_raw_json_cache(cache_key: SupportsBytes) -> bytes:
|
||||
"""
|
||||
Read a JSON cache file stored in a temporary directory. Keyword arguments
|
||||
are passed to :py:function:`json.load` as is.
|
||||
Read a JSON cache file stored in a temporary directory.
|
||||
|
||||
:param cache_key: cache key object
|
||||
:param raw: skip decoding and return raw file contents instead
|
||||
|
||||
:return: decoded data or ``None``
|
||||
:return: raw JSON file contents or empty byte string
|
||||
"""
|
||||
|
||||
cache = _get_cache_path(cache_key)
|
||||
if not cache.is_file():
|
||||
return None
|
||||
return b""
|
||||
|
||||
with open(cache, "rb") as file:
|
||||
if raw:
|
||||
return file.read()
|
||||
return json.load(file, **kwargs)
|
||||
return file.read()
|
||||
|
|
|
@ -4,32 +4,21 @@
|
|||
|
||||
"""
|
||||
CLI subcommands for everything Bugzilla.
|
||||
|
||||
This Python module also defines some regular expressions.
|
||||
|
||||
``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 collections.abc import Collection
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from xmlrpc.client import DateTime
|
||||
|
||||
import click
|
||||
import gentoopm
|
||||
import pydantic_core
|
||||
from tabulate import tabulate
|
||||
|
||||
from find_work.cache import (
|
||||
read_json_cache,
|
||||
write_json_cache,
|
||||
read_raw_json_cache,
|
||||
write_raw_json_cache,
|
||||
)
|
||||
from find_work.cli import Message, Options, ProgressDots
|
||||
from find_work.constants import BUGZILLA_URL
|
||||
|
@ -45,40 +34,24 @@ with warnings.catch_warnings():
|
|||
import bugzilla
|
||||
from bugzilla.bug import Bug
|
||||
|
||||
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]:
|
||||
def _bugs_from_raw_json(raw_json: str | bytes) -> list[Bug]:
|
||||
data: list[dict] = pydantic_core.from_json(raw_json)
|
||||
with requests_session() as session:
|
||||
bz = bugzilla.Bugzilla(BUGZILLA_URL, requests_session=session)
|
||||
bz = bugzilla.Bugzilla(BUGZILLA_URL, requests_session=session,
|
||||
force_rest=True)
|
||||
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 _bugs_to_raw_json(data: Collection[Bug]) -> bytes:
|
||||
raw_data = [bug.get_raw_data() for bug in data]
|
||||
return pydantic_core.to_json(raw_data, exclude_none=True)
|
||||
|
||||
|
||||
def _fetch_bugs(options: Options, **kwargs: Any) -> list[Bug]:
|
||||
with requests_session() as session:
|
||||
bz = bugzilla.Bugzilla(BUGZILLA_URL, requests_session=session)
|
||||
bz = bugzilla.Bugzilla(BUGZILLA_URL, requests_session=session,
|
||||
force_rest=True)
|
||||
query = bz.build_query(
|
||||
short_desc=options.bugzilla.short_desc or None,
|
||||
product=options.bugzilla.product or None,
|
||||
|
@ -93,7 +66,7 @@ def _fetch_bugs(options: Options, **kwargs: Any) -> list[Bug]:
|
|||
return bz.query(query)
|
||||
|
||||
|
||||
def _collect_bugs(data: Iterable[Bug], options: Options) -> list[BugView]:
|
||||
def _collect_bugs(data: Collection[Bug], options: Options) -> list[BugView]:
|
||||
if options.only_installed:
|
||||
pm = gentoopm.get_package_manager()
|
||||
|
||||
|
@ -105,7 +78,7 @@ def _collect_bugs(data: Iterable[Bug], options: Options) -> list[BugView]:
|
|||
if package not in pm.installed:
|
||||
continue
|
||||
|
||||
date = time.strftime("%F", bug.last_change_time.timetuple())
|
||||
date = datetime.fromisoformat(bug.last_change_time).date().isoformat()
|
||||
item = BugView(bug.id, date, bug.assigned_to, bug.summary)
|
||||
result.append(item)
|
||||
return result
|
||||
|
@ -117,12 +90,11 @@ def _list_bugs(cmd: str, options: Options, **filters: Any) -> None:
|
|||
|
||||
options.say(Message.CACHE_LOAD)
|
||||
with dots():
|
||||
cached_data = read_json_cache(options.cache_key,
|
||||
object_hook=as_datetime)
|
||||
if cached_data is not None:
|
||||
raw_data = read_raw_json_cache(options.cache_key)
|
||||
if raw_data:
|
||||
options.say(Message.CACHE_READ)
|
||||
with dots():
|
||||
data = _bugs_from_json(cached_data)
|
||||
data = _bugs_from_raw_json(raw_data)
|
||||
else:
|
||||
options.vecho("Fetching data from Bugzilla API", nl=False, err=True)
|
||||
with dots():
|
||||
|
@ -132,8 +104,8 @@ def _list_bugs(cmd: str, options: Options, **filters: Any) -> None:
|
|||
return
|
||||
options.say(Message.CACHE_WRITE)
|
||||
with dots():
|
||||
json_data = _bugs_to_json(data)
|
||||
write_json_cache(json_data, options.cache_key, cls=BugEncoder)
|
||||
raw_json = _bugs_to_raw_json(data)
|
||||
write_raw_json_cache(raw_json, options.cache_key)
|
||||
|
||||
bumps = _collect_bugs(data, options)
|
||||
if len(bumps) != 0:
|
||||
|
|
|
@ -13,7 +13,7 @@ from click_aliases import ClickAliasedGroup
|
|||
|
||||
from find_work.cli import Options
|
||||
from find_work.config import ConfigAlias, ConfigModuleOption, load_config
|
||||
from find_work.types import CliOptionKind
|
||||
from find_work.types._config import CliOptionKind
|
||||
|
||||
|
||||
def _new_click_option(opt: ConfigModuleOption) -> Callable:
|
||||
|
|
|
@ -5,24 +5,44 @@
|
|||
""" CLI subcommands for Gentoo Packages website. """
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
|
||||
import click
|
||||
import gentoopm
|
||||
from pydantic import TypeAdapter
|
||||
from sortedcontainers import SortedDict, SortedSet
|
||||
from tabulate import tabulate
|
||||
|
||||
from find_work.cache import (
|
||||
read_json_cache,
|
||||
write_json_cache,
|
||||
read_raw_json_cache,
|
||||
write_raw_json_cache,
|
||||
)
|
||||
from find_work.cli import (
|
||||
Message,
|
||||
Options,
|
||||
ProgressDots,
|
||||
)
|
||||
from find_work.constants import (
|
||||
PGO_BASE_URL,
|
||||
PGO_API_URL,
|
||||
)
|
||||
from find_work.types import (
|
||||
VersionBump,
|
||||
VersionPart,
|
||||
)
|
||||
from find_work.types._pgo import (
|
||||
GraphQlResponse,
|
||||
OutdatedPackage,
|
||||
PkgCheckResult,
|
||||
StableCandidate,
|
||||
)
|
||||
from find_work.cli import Message, Options, ProgressDots
|
||||
from find_work.constants import PGO_BASE_URL, PGO_API_URL
|
||||
from find_work.types import VersionBump, VersionPart
|
||||
from find_work.utils import aiohttp_session
|
||||
|
||||
OutdatedPackageSet = frozenset[OutdatedPackage]
|
||||
PkgCheckResultSet = frozenset[PkgCheckResult]
|
||||
StableCandidateSet = frozenset[StableCandidate]
|
||||
|
||||
async def _fetch_outdated() -> list[dict]:
|
||||
|
||||
async def _fetch_outdated() -> OutdatedPackageSet:
|
||||
query = """query {
|
||||
outdatedPackages{
|
||||
Atom
|
||||
|
@ -34,24 +54,22 @@ async def _fetch_outdated() -> list[dict]:
|
|||
async with aiohttp_session() as session:
|
||||
async with session.post(PGO_API_URL, json={"query": query},
|
||||
raise_for_status=True) as response:
|
||||
data = await response.json()
|
||||
return data.get("data", {}).get("outdatedPackages", [])
|
||||
raw_data = await response.read()
|
||||
|
||||
graphql = GraphQlResponse.model_validate_json(raw_data)
|
||||
return graphql.data.outdated
|
||||
|
||||
|
||||
def _collect_version_bumps(data: Iterable[dict],
|
||||
def _collect_version_bumps(data: OutdatedPackageSet,
|
||||
options: Options) -> SortedSet[VersionBump]:
|
||||
if options.only_installed:
|
||||
pm = gentoopm.get_package_manager()
|
||||
|
||||
result: SortedSet[VersionBump] = SortedSet()
|
||||
for item in data:
|
||||
bump = VersionBump(item["Atom"],
|
||||
item.get("GentooVersion", "(unknown)"),
|
||||
item.get("NewestVersion", "(unknown)"))
|
||||
|
||||
if options.only_installed and bump.atom not in pm.installed:
|
||||
if options.only_installed and item.atom not in pm.installed:
|
||||
continue
|
||||
result.add(bump)
|
||||
result.add(item.as_version_bump)
|
||||
return result
|
||||
|
||||
|
||||
|
@ -68,8 +86,12 @@ async def _outdated(options: Options) -> None:
|
|||
|
||||
options.say(Message.CACHE_LOAD)
|
||||
with dots():
|
||||
data = read_json_cache(options.cache_key)
|
||||
if data is None:
|
||||
raw_data = read_raw_json_cache(options.cache_key)
|
||||
if raw_data:
|
||||
options.say(Message.CACHE_READ)
|
||||
with dots():
|
||||
data = TypeAdapter(OutdatedPackageSet).validate_json(raw_data)
|
||||
else:
|
||||
options.vecho("Fetching data from Gentoo Packages API",
|
||||
nl=False, err=True)
|
||||
with dots():
|
||||
|
@ -79,7 +101,10 @@ async def _outdated(options: Options) -> None:
|
|||
return
|
||||
options.say(Message.CACHE_WRITE)
|
||||
with dots():
|
||||
write_json_cache(data, options.cache_key)
|
||||
raw_json = TypeAdapter(OutdatedPackageSet).dump_json(
|
||||
data, by_alias=True, exclude_none=True
|
||||
)
|
||||
write_raw_json_cache(raw_json, options.cache_key)
|
||||
|
||||
no_work = True
|
||||
for bump in _collect_version_bumps(data, options):
|
||||
|
@ -96,24 +121,18 @@ async def _outdated(options: Options) -> None:
|
|||
options.say(Message.NO_WORK)
|
||||
|
||||
|
||||
async def _fetch_maintainer_stabilization(maintainer: str) -> list[dict]:
|
||||
url = f"{PGO_BASE_URL}/maintainer/{maintainer}/stabilization.json"
|
||||
async def _fetch_maintainer_stabilization(maintainer: str) -> PkgCheckResultSet:
|
||||
|
||||
url = PGO_BASE_URL + f"/maintainer/{maintainer}/stabilization.json"
|
||||
async with aiohttp_session() as session:
|
||||
async with session.get(url, raise_for_status=True) as response:
|
||||
data = await response.json()
|
||||
raw_data = await response.read()
|
||||
|
||||
# bring data to a common structure
|
||||
return [
|
||||
{
|
||||
"Atom": f"{item['category']}/{item['package']}",
|
||||
"Version": item["version"],
|
||||
"Message": item["message"],
|
||||
}
|
||||
for item in data
|
||||
]
|
||||
data = TypeAdapter(StableCandidateSet).validate_json(raw_data)
|
||||
return frozenset(item.as_pkgcheck_result for item in data)
|
||||
|
||||
|
||||
async def _fetch_all_stabilization() -> list[dict]:
|
||||
async def _fetch_all_stabilization() -> PkgCheckResultSet:
|
||||
query = """query {
|
||||
pkgCheckResults(Class: "StableRequest") {
|
||||
Atom
|
||||
|
@ -125,27 +144,29 @@ async def _fetch_all_stabilization() -> list[dict]:
|
|||
async with aiohttp_session() as session:
|
||||
async with session.post(PGO_API_URL, json={"query": query},
|
||||
raise_for_status=True) as response:
|
||||
data = await response.json()
|
||||
return data.get("data", {}).get("pkgCheckResults", [])
|
||||
raw_data = await response.read()
|
||||
|
||||
graphql = GraphQlResponse.model_validate_json(raw_data)
|
||||
return graphql.data.pkgcheck
|
||||
|
||||
|
||||
async def _fetch_stabilization(options: Options) -> list[dict]:
|
||||
async def _fetch_stabilization(options: Options) -> PkgCheckResultSet:
|
||||
if options.maintainer:
|
||||
return await _fetch_maintainer_stabilization(options.maintainer)
|
||||
return await _fetch_all_stabilization()
|
||||
|
||||
|
||||
def _collect_stable_candidates(data: list[dict],
|
||||
def _collect_stable_candidates(data: PkgCheckResultSet,
|
||||
options: Options) -> SortedDict[str, str]:
|
||||
if options.only_installed:
|
||||
pm = gentoopm.get_package_manager()
|
||||
|
||||
result: SortedDict[str, str] = SortedDict()
|
||||
for item in data:
|
||||
if options.only_installed and item["Atom"] not in pm.installed:
|
||||
if options.only_installed and item.atom not in pm.installed:
|
||||
continue
|
||||
key = "-".join([item["Atom"], item["Version"]])
|
||||
result[key] = item["Message"]
|
||||
key = "-".join([item.atom, item.version])
|
||||
result[key] = item.message
|
||||
return result
|
||||
|
||||
|
||||
|
@ -154,8 +175,12 @@ async def _stabilization(options: Options) -> None:
|
|||
|
||||
options.say(Message.CACHE_LOAD)
|
||||
with dots():
|
||||
data = read_json_cache(options.cache_key)
|
||||
if data is None:
|
||||
raw_data = read_raw_json_cache(options.cache_key)
|
||||
if raw_data:
|
||||
options.say(Message.CACHE_READ)
|
||||
with dots():
|
||||
data = TypeAdapter(PkgCheckResultSet).validate_json(raw_data)
|
||||
else:
|
||||
options.vecho("Fetching data from Gentoo Packages API",
|
||||
nl=False, err=True)
|
||||
with dots():
|
||||
|
@ -165,7 +190,10 @@ async def _stabilization(options: Options) -> None:
|
|||
return
|
||||
options.say(Message.CACHE_WRITE)
|
||||
with dots():
|
||||
write_json_cache(data, options.cache_key)
|
||||
raw_data = TypeAdapter(PkgCheckResultSet).dump_json(
|
||||
data, by_alias=True, exclude_none=True
|
||||
)
|
||||
write_raw_json_cache(raw_data, options.cache_key)
|
||||
|
||||
candidates = _collect_stable_candidates(data, options)
|
||||
if len(candidates) != 0:
|
||||
|
@ -193,6 +221,9 @@ def outdated(options: Options, version_part: VersionPart | None = None) -> None:
|
|||
@click.command()
|
||||
@click.pass_obj
|
||||
def stabilization(options: Options) -> None:
|
||||
""" Find outdated packages. """
|
||||
"""
|
||||
Find stable candidates.
|
||||
"""
|
||||
|
||||
options.cache_key.feed("stabilization")
|
||||
asyncio.run(_stabilization(options))
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
""" CLI subcommands for everything Repology. """
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Collection
|
||||
|
||||
import click
|
||||
import gentoopm
|
||||
|
@ -17,14 +17,15 @@ from repology_client.types import Package
|
|||
from sortedcontainers import SortedSet
|
||||
|
||||
from find_work.cache import (
|
||||
read_json_cache,
|
||||
write_json_cache,
|
||||
read_raw_json_cache,
|
||||
write_raw_json_cache,
|
||||
)
|
||||
from find_work.cli import Message, Options, ProgressDots
|
||||
from find_work.types import VersionBump, VersionPart
|
||||
from find_work.utils import aiohttp_session
|
||||
|
||||
ProjectsMapping = dict[str, set[Package]]
|
||||
PackageSet = set[Package]
|
||||
ProjectsMapping = dict[str, PackageSet]
|
||||
|
||||
|
||||
async def _fetch_outdated(options: Options) -> ProjectsMapping:
|
||||
|
@ -38,17 +39,7 @@ async def _fetch_outdated(options: Options) -> ProjectsMapping:
|
|||
session=session, **filters)
|
||||
|
||||
|
||||
def _projects_from_raw_json(raw_json: str | bytes) -> ProjectsMapping:
|
||||
projects_adapter = TypeAdapter(ProjectsMapping)
|
||||
return projects_adapter.validate_json(raw_json)
|
||||
|
||||
|
||||
def _projects_to_raw_json(data: ProjectsMapping) -> bytes:
|
||||
projects_adapter = TypeAdapter(ProjectsMapping)
|
||||
return projects_adapter.dump_json(data, exclude_none=True)
|
||||
|
||||
|
||||
def _collect_version_bumps(data: Iterable[set[Package]],
|
||||
def _collect_version_bumps(data: Collection[PackageSet],
|
||||
options: Options) -> SortedSet[VersionBump]:
|
||||
pm = gentoopm.get_package_manager()
|
||||
|
||||
|
@ -59,7 +50,7 @@ def _collect_version_bumps(data: Iterable[set[Package]],
|
|||
|
||||
for pkg in packages:
|
||||
if pkg.status == "outdated" and pkg.repo == options.repology.repo:
|
||||
# ``pkg.version`` can contain spaces, better avoid it!
|
||||
# "pkg.version" can contain spaces, better avoid it!
|
||||
origversion = pkg.origversion or pkg.version
|
||||
atom = pm.Atom(f"={pkg.visiblename}-{origversion}")
|
||||
|
||||
|
@ -84,11 +75,11 @@ async def _outdated(options: Options) -> None:
|
|||
|
||||
options.say(Message.CACHE_LOAD)
|
||||
with dots():
|
||||
raw_cached_data = read_json_cache(options.cache_key, raw=True)
|
||||
if raw_cached_data is not None:
|
||||
raw_data = read_raw_json_cache(options.cache_key)
|
||||
if raw_data:
|
||||
options.say(Message.CACHE_READ)
|
||||
with dots():
|
||||
data = _projects_from_raw_json(raw_cached_data)
|
||||
data = TypeAdapter(ProjectsMapping).validate_json(raw_data)
|
||||
else:
|
||||
options.vecho("Fetching data from Repology API", nl=False, err=True)
|
||||
try:
|
||||
|
@ -99,8 +90,10 @@ async def _outdated(options: Options) -> None:
|
|||
return
|
||||
options.say(Message.CACHE_WRITE)
|
||||
with dots():
|
||||
raw_json_data = _projects_to_raw_json(data)
|
||||
write_json_cache(raw_json_data, options.cache_key, raw=True)
|
||||
raw_json = TypeAdapter(ProjectsMapping).dump_json(
|
||||
data, exclude_none=True
|
||||
)
|
||||
write_raw_json_cache(raw_json, options.cache_key)
|
||||
|
||||
no_work = True
|
||||
for bump in _collect_version_bumps(data.values(), options):
|
||||
|
|
|
@ -17,7 +17,7 @@ from platformdirs import PlatformDirs
|
|||
|
||||
import find_work.data
|
||||
from find_work.constants import DEFAULT_CONFIG, ENTITY, PACKAGE
|
||||
from find_work.types import CliOptionKind
|
||||
from find_work.types._config import CliOptionKind
|
||||
|
||||
|
||||
# FIXME: Find out how to use Pydantic for type validation
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
# No warranty
|
||||
|
||||
"""
|
||||
Type definitions for the application, implemented as enums and Pydantic models.
|
||||
Public type definitions for the application, implemented as enums and Pydantic
|
||||
models.
|
||||
"""
|
||||
|
||||
from dataclasses import field
|
||||
from enum import Enum, StrEnum, auto
|
||||
from enum import StrEnum, auto
|
||||
from itertools import zip_longest
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
@ -71,10 +72,3 @@ class BugView:
|
|||
last_change_date: str = field(compare=False)
|
||||
assigned_to: str = field(compare=False)
|
||||
summary: str = field(compare=False)
|
||||
|
||||
|
||||
class CliOptionKind(Enum):
|
||||
SIMPLE = auto()
|
||||
|
||||
OPTION = auto()
|
||||
FLAG = auto()
|
|
@ -0,0 +1,16 @@
|
|||
# SPDX-License-Identifier: WTFPL
|
||||
# SPDX-FileCopyrightText: 2024 Anna <cyber@sysrq.in>
|
||||
# No warranty
|
||||
|
||||
"""
|
||||
Type definitions for configuration file.
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class CliOptionKind(Enum):
|
||||
SIMPLE = auto()
|
||||
|
||||
OPTION = auto()
|
||||
FLAG = auto()
|
|
@ -0,0 +1,106 @@
|
|||
# SPDX-License-Identifier: WTFPL
|
||||
# SPDX-FileCopyrightText: 2024 Anna <cyber@sysrq.in>
|
||||
# No warranty
|
||||
|
||||
"""
|
||||
Internal type definitions for Gentoo Packages GraphQL API, implemented as
|
||||
Pydantic models.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from find_work.types import VersionBump
|
||||
|
||||
|
||||
class OutdatedPackage(BaseModel):
|
||||
"""
|
||||
Information from Repology about an outdated package in the Gentoo tree.
|
||||
"""
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
#: The atom of the affected package.
|
||||
atom: str = Field(alias="Atom")
|
||||
|
||||
#: The latest version of the package that is present in the Gentoo tree.
|
||||
old_version: str = Field(alias="GentooVersion", default="(unknown)")
|
||||
|
||||
#: The latest version of the package that is present upstream.
|
||||
new_version: str = Field(alias="NewestVersion", default="(unknown)")
|
||||
|
||||
@property
|
||||
def as_version_bump(self) -> VersionBump:
|
||||
"""
|
||||
Equivalent :py:class:`find_work.types.VersionBump` object.
|
||||
"""
|
||||
|
||||
return VersionBump(self.atom, self.old_version, self.new_version)
|
||||
|
||||
|
||||
class PkgCheckResult(BaseModel):
|
||||
"""
|
||||
Single warning from pkgcheck for a package version.
|
||||
"""
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
#: Atom of the package that is affected by this pkgcheck warning.
|
||||
atom: str = Field(alias="Atom")
|
||||
|
||||
# Version of the package that is affected by this pkgcheck warning.
|
||||
version: str = Field(alias="Version")
|
||||
|
||||
# Message of this warning, e.g. 'uses deprecated EAPI 5'.
|
||||
message: str = Field(alias="Message")
|
||||
|
||||
|
||||
class StableCandidate(BaseModel):
|
||||
"""
|
||||
Stabilization candidate representation.
|
||||
"""
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
#: Category name.
|
||||
category: str
|
||||
|
||||
#: Package name.
|
||||
package: str
|
||||
|
||||
#: Package version.
|
||||
version: str
|
||||
|
||||
#: Pkgcheck message.
|
||||
message: str
|
||||
|
||||
@property
|
||||
def as_pkgcheck_result(self) -> PkgCheckResult:
|
||||
"""
|
||||
Equivalent :py:class:`PkgCheckResult` object.
|
||||
"""
|
||||
|
||||
data = {
|
||||
"Atom": "/".join([self.category, self.package]),
|
||||
"Version": self.version,
|
||||
"Message": self.message,
|
||||
}
|
||||
return PkgCheckResult.model_validate(data)
|
||||
|
||||
|
||||
class GraphQlData(BaseModel):
|
||||
"""
|
||||
Data returned by GraphQL.
|
||||
"""
|
||||
|
||||
#: Results of outdatedPackages query.
|
||||
outdated: frozenset[OutdatedPackage] = Field(alias="outdatedPackages",
|
||||
default=frozenset())
|
||||
|
||||
#: Results of pkgCheckResults query.
|
||||
pkgcheck: frozenset[PkgCheckResult] = Field(alias="pkgCheckResults",
|
||||
default=frozenset())
|
||||
|
||||
|
||||
class GraphQlResponse(BaseModel):
|
||||
"""
|
||||
Root GraphQL response.
|
||||
"""
|
||||
|
||||
data: GraphQlData = Field(default_factory=GraphQlData)
|
|
@ -24,6 +24,7 @@ dependencies = [
|
|||
"pkgcheck",
|
||||
"platformdirs<5,>=4",
|
||||
"pydantic<3,>=2",
|
||||
"pydantic-core<3,>=2",
|
||||
"python-bugzilla",
|
||||
"repology-client<2,>=0.0.2",
|
||||
"requests<3,>=2",
|
||||
|
@ -52,7 +53,6 @@ docs = [
|
|||
test = [
|
||||
"pkgcore",
|
||||
"pytest",
|
||||
"pytest-recording",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
@ -78,7 +78,7 @@ include = [
|
|||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--doctest-modules --block-network"
|
||||
addopts = "--doctest-modules"
|
||||
|
||||
[tool.mypy]
|
||||
disallow_untyped_defs = true
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
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 +0,0 @@
|
|||
[{"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": "---"}]
|
|
@ -1,3 +0,0 @@
|
|||
SPDX-FileCopyrightText: 2024 Gentoo Authors
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
|
@ -1,20 +0,0 @@
|
|||
# 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))
|
|
@ -8,7 +8,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
from find_work.config import Config
|
||||
from find_work.types import CliOptionKind
|
||||
from find_work.types._config import CliOptionKind
|
||||
|
||||
|
||||
def test_alias_empty():
|
||||
|
|
|
@ -7,33 +7,7 @@ from repology_client.types import Package
|
|||
|
||||
from find_work.types import VersionBump
|
||||
from find_work.cli import Options
|
||||
from find_work.cli.repology import (
|
||||
_collect_version_bumps,
|
||||
_projects_from_raw_json,
|
||||
_projects_to_raw_json,
|
||||
)
|
||||
|
||||
|
||||
def test_projects_json_roundtrip():
|
||||
data = {
|
||||
"firefox": {
|
||||
Package(
|
||||
repo="gentoo",
|
||||
visiblename="www-client/firefox",
|
||||
version="9999",
|
||||
status="test",
|
||||
licenses=frozenset(["GPL-2", "LGPL-2.1", "MPL-2.0"]),
|
||||
),
|
||||
Package(
|
||||
repo="gentoo",
|
||||
visiblename="www-client/firefox-bin",
|
||||
version="9999",
|
||||
status="test",
|
||||
licenses=frozenset(["GPL-2", "LGPL-2.1", "MPL-2.0"]),
|
||||
),
|
||||
},
|
||||
}
|
||||
assert data == _projects_from_raw_json(_projects_to_raw_json(data))
|
||||
from find_work.cli.repology import _collect_version_bumps
|
||||
|
||||
|
||||
def test_collect_version_bumps():
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -8,7 +8,6 @@ env_list = py3{11,12}, lint
|
|||
[testenv]
|
||||
description = run the tests + mypy
|
||||
deps =
|
||||
aiodns
|
||||
mypy
|
||||
sortedcontainers-stubs
|
||||
types-requests
|
||||
|
@ -17,8 +16,8 @@ deps =
|
|||
extras =
|
||||
test
|
||||
commands =
|
||||
mypy find_work tests
|
||||
pytest -vv {tty:--color=yes} {posargs}
|
||||
mypy find_work tests
|
||||
|
||||
[testenv:lint]
|
||||
description = run the linters
|
||||
|
|
Loading…
Reference in New Issue