Skip to content

Commit

Permalink
Turn off formatting: See psf/black#4268
Browse files Browse the repository at this point in the history
  • Loading branch information
kwk committed Mar 8, 2024
1 parent 7bb8457 commit 527456b
Show file tree
Hide file tree
Showing 14 changed files with 499 additions and 347 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repos:
# See https://black.readthedocs.io/en/stable/integrations/source_version_control.html
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1
rev: 24.2.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
Expand Down
12 changes: 12 additions & 0 deletions src/snapshot_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,15 @@
# from .mixins import *
# from .error import *
# from . import *


def load_tests(loader, tests, ignore):
"""We want unittest to pick up all of our doctests
See https://docs.python.org/3/library/unittest.html#load-tests-protocol
See https://stackoverflow.com/a/27171468
"""
import doctest

tests.addTests(doctest.DocTestSuite())
return tests
4 changes: 4 additions & 0 deletions src/snapshot_manager/build_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class BuildStatus(enum.StrEnum):
CANCELED = "canceled" # The build has been cancelled manually.
WAITING = "waiting" # Task is waiting for something else to finish.

@classmethod
def all_states(cls) -> list["BuildStatus"]:
return [s for s in BuildStatus]

def toIcon(self) -> str:
"""Get a github markdown icon for the given build status
Expand Down
44 changes: 16 additions & 28 deletions src/snapshot_manager/config_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
ConfigMixin
"""


import datetime
import re
import typing
Expand Down Expand Up @@ -59,8 +58,8 @@ def yyyymmdd(self) -> str:
Example: Adjust the default time and print this yyyymmdd property
>>> SnapshotManager.default_datetime = datetime.date(year=2024, month=2, day=29)
>>> print(SnapshotManager().yyyymmdd)
>>> ConfigMixin.default_datetime = datetime.date(year=2024, month=2, day=29)
>>> print(ConfigMixin().yyyymmdd)
20240229
"""
return self.default_datetime.strftime("%Y%m%d")
Expand Down Expand Up @@ -93,25 +92,14 @@ def shorten_text(cls, text: str, max_length: typing.Optional[int] = None) -> str

return text[:max_length]

@classmethod
def wrap_file_in_md_code_fence(cls, context_file: pathlib.Path) -> None:
"""Surround context file with markdown code fence and shorten it appropriately.
Args:
context_file (pathlib.Path): A file path object pointing to a temporary build log for example
Example:
>>> import tempfile
>>> p = SnapshotManager.write_to_temp_file("0123456789")
>>> print(p.read_text())
0123456789
"""
cls.shorten_file(context_file)
with open(context_file, "r") as original:
data = original.read()
with open(context_file, "w") as modified:
modified.write(f"\n```\n{data}\n```\n")
def test_get_build_url(self):
"""Get build URL"""
self.assertTrue(
self.mgr.get_build_url(
copr_ownername="foo", copr_projectname="bar", build_id=1234
),
"https://copr.fedorainfracloud.org/coprs/g/foo/bar/build/1234/",
)

@property
def github_repo(self) -> str:
Expand All @@ -129,13 +117,13 @@ def get_arch_from_chroot(cls, chroot: str) -> str:
Example:
>>> print(SnapshotManager.get_arch_from_chroot("fedora-rawhide-x86_64"))
>>> print(ConfigMixin.get_arch_from_chroot("fedora-rawhide-x86_64"))
x86_64
>>> print(SnapshotManager.get_arch_from_chroot("fedora-40-ppc64le"))
>>> print(ConfigMixin.get_arch_from_chroot("fedora-40-ppc64le"))
ppc64le
>>> print(SnapshotManager.get_arch_from_chroot("fedora-rawhide-NEWARCH"))
>>> print(ConfigMixin.get_arch_from_chroot("fedora-rawhide-NEWARCH"))
NEWARCH
"""
match = regex.search(pattern=r"[^-]+-[^-]+-\K[^\s]+", string=chroot)
Expand All @@ -155,13 +143,13 @@ def get_os_from_chroot(cls, chroot: str) -> str:
Examples:
>>> print(SnapshotManager.get_os_from_chroot("fedora-rawhide-x86_64"))
>>> print(ConfigMixin.get_os_from_chroot("fedora-rawhide-x86_64"))
fedora-rawhide
>>> print(SnapshotManager.get_os_from_chroot("fedora-40-ppc64le"))
>>> print(ConfigMixin.get_os_from_chroot("fedora-40-ppc64le"))
fedora-40
>>> print(SnapshotManager.get_os_from_chroot("fedora-rawhide-NEWARCH"))
>>> print(ConfigMixin.get_os_from_chroot("fedora-rawhide-NEWARCH"))
fedora-rawhide
"""
match = re.search(pattern=r"[^-]+-[0-9,rawhide]+", string=chroot)
Expand Down
3 changes: 2 additions & 1 deletion src/snapshot_manager/copr_client_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ def has_all_good_builds(
Example: Check with a not existing copr project
>>> SnapshotManager().has_all_good_builds(copr_ownername="non-existing-owner", copr_projectname="non-existing-project")
>>> import snapshot_manager
>>> snapshot_manager.SnapshotManager().has_all_good_builds(copr_ownername="non-existing-owner", copr_projectname="non-existing-project")
Traceback (most recent call last):
ValueError: copr project non-existing-owner/non-existing-project does not exist
"""
Expand Down
201 changes: 201 additions & 0 deletions src/snapshot_manager/file_access_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""
FileAccessMixin
"""

import typing
import pathlib
import requests
import config_mixin


class FileAccessMixin(config_mixin.ConfigMixin):
"""Any class that needs to access to the filesystem can derive from this class"""

def __init__(self, github_token: typing.Optional[str] = None, **kwargs):
"""
Initializes the mixin.
"""
super().__init__(**kwargs)

@classmethod
def _run_cmd(cls, cmd: str, timeout_secs: int = 5) -> tuple[int, str, str]:
"""Runs the given command and returns the output (stdout and stderr) if any.
Args:
cmd (str): Command to run, e.g. "ls -lha ."
Returns:
tuple[int, str, str]: The command exit code and it's stdout and sterr
"""
import shlex, subprocess

proc = subprocess.run(
shlex.split(cmd), timeout=timeout_secs, capture_output=True
)
return proc.returncode, proc.stdout.decode(), proc.stderr.decode()

@classmethod
@typing.overload
def write_to_temp_file(cls, content: bytes) -> pathlib.Path: ...

@classmethod
@typing.overload
def write_to_temp_file(cls, text: str) -> pathlib.Path: ...

@classmethod
def write_to_temp_file(cls, content: str | bytes) -> pathlib.Path:
"""Creates a named temporary file that isn't deleted and writes content to it.
Args:
content (str|bytes): String or bytes to be written to the file
Raises:
ValueError: If the content has an unsupported type
Returns:
pathlib.Path: Path object of the temporary file created
Example: Write a string to a temporary file
>>> p = FileAccessMixin.write_to_temp_file("foo")
>>> data = p.read_text()
>>> print(data)
foo
Example: Write bytes to a temporary file
>>> p = FileAccessMixin.write_to_temp_file("bar".encode())
>>> print(p.read_text())
bar
Example: Write unsupported content to temp file
>>> p = FileAccessMixin.write_to_temp_file(123)
Traceback (most recent call last):
ValueError: unsupported content type to write to temporary file
"""
import tempfile

f = tempfile.NamedTemporaryFile(delete_on_close=False, delete=False)
p = pathlib.Path(f.name)
if isinstance(content, bytes):
p.write_bytes(content)
elif isinstance(content, str):
p.write_text(content)
else:
raise ValueError("unsupported content type to write to temporary file")
return p

@classmethod
def wrap_file_in_md_code_fence(cls, context_file: pathlib.Path) -> None:
"""Surround context file with markdown code fence and shorten it appropriately.
Args:
context_file (pathlib.Path): A file path object pointing to a temporary build log for example
Example:
>>> p = FileAccessMixin.write_to_temp_file("0123456789")
>>> print(p.read_text())
0123456789
"""
cls.shorten_file(context_file)
with open(context_file, "r") as original:
data = original.read()
with open(context_file, "w") as modified:
modified.write(f"\n```\n{data}\n```\n")

@classmethod
def read_url_response_into_file(cls, url: str) -> pathlib.Path:
"""Fetch the given URL and store it in a temporary file whose name is returned.
Args:
url (str): URL to GET
Returns:
pathlib.Path: Path object of the temporary file to which the GET response was written to.
"""
response = requests.get(url)
return cls.write_to_temp_file(response.content)

@classmethod
def grep_file(
cls,
pattern: str,
filepath: typing.Union[str, pathlib.Path],
lines_before: typing.Optional[int] = 0,
lines_after: typing.Optional[int] = 0,
case_insensitive: typing.Optional[bool] = True,
extra_args: typing.Optional[str] = None,
grep_bin: typing.Optional[str] = "grep",
) -> tuple[int, str, str]:
"""Runs the grep binary on the filepath and includes lines before and after repsectively.
Args:
pattern (str): The pattern to find
filepath (typing.Union[str,pathlib.Path]): The file to search for the pattern
lines_before (typing.Optional[int], optional): Includes N-lines before a given match. Defaults to 0.
lines_after (typing.Optional[int], optional): Includes N-lines after a given match. Defaults to 0.
case_insensitive (typing.Optional[bool], optional): Ignores cases. Defaults to True.
extra_args (typing.Optiona[str], optional): A string of grep extra arguments (e.g. "-P"). Defaults to None.
Raises:
ValueError: If the pattern is empty
ValueError: If the lines_before is negative
ValueError: If the lines_after is negative
Returns:
tuple[int, str, str]: return code, stdout, stderr
"""
if pattern is None or pattern == "":
raise ValueError(f"pattern is invalid:{pattern}")

if lines_before is None or lines_before < 0:
raise ValueError(f"lines_before must be zero or a positive integer")

if lines_after is None or lines_after < 0:
raise ValueError(f"lines_after must be zero or a positive integer")

opts = []
if case_insensitive:
opts.append("-i")
if lines_before > 0:
opts.append(f"--before-context={lines_before}")

if lines_after > 0:
opts.append(f"--after-context={lines_after}")

if isinstance(filepath, pathlib.Path):
filepath = filepath.resolve()

if extra_args is None:
extra_args = ""

cmd = f"{grep_bin} {" ".join(opts)} {extra_args} '{pattern}' {filepath}"
return cls._run_cmd(cmd)

@classmethod
def grep_url(cls, url, **kw_args) -> tuple[str, pathlib.Path]:
"""GETs the given url and passes all other parameters on to grep_file
See grep_file for knowing what arguments are accepted for kw_args.
Args:
url (_type_): URL to get
Returns:
str: Potential matches
"""
file = cls.read_url_response_into_file(url=url)
return cls.grep_file(filepath=file, **kw_args), file

@classmethod
def gunzip(cls, f: tuple[str, pathlib.Path]) -> pathlib.Path:
# Unzip log file on the fly if we need to
if str(f).endswith(".gz"):
unzipped_file = str(f).removesuffix(".gz")
retcode, stdout, stderr = cls._run_cmd(cmd=f"gunzip -kf {f}")
if retcode != 0:
raise Exception(f"Failed to gunzip build log '{f}': {stderr}")
f = unzipped_file
return pathlib.Path(str(f))
5 changes: 2 additions & 3 deletions src/snapshot_manager/github_client_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import github.GithubException

import config_mixin
import build_status


# pylint: disable=too-few-public-methods
Expand All @@ -27,8 +28,6 @@ def __init__(self, github_token: typing.Optional[str] = None, **kwargs):
github_token = os.getenv(self._default_github_token_env)
self.github = github.Github(login_or_token=github_token)

# This acts a cache for chroots to reduce queries being made to copr
# TODO(kwk): Add mutex to protec this shared resource.
super().__init__(**kwargs)

def update_build_status_in_github_issue(
Expand Down Expand Up @@ -102,7 +101,7 @@ def create_or_get_todays_github_issue(
> The CI system will update this very comment over time to list the progress.
> Please note, that logs snippets will be limited to {self.default_max_context_bytes} bytes.
{self.markdown_build_matrix()}
{self.markdown_build_status_matrix()}
{self._update_marker}
""",
Expand Down
Loading

0 comments on commit 527456b

Please sign in to comment.