Skip to content

Commit

Permalink
libs: Add envoy.github.abstract (#22)
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Northey <ryan@synca.io>
  • Loading branch information
phlax committed Aug 24, 2021
1 parent 5dfcdc8 commit 40cd200
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
- envoy.base.utils
- envoy.base.runner
- envoy.base.checker
- envoy.github.abstract
python-version:
- 3.8
- 3.9
Expand Down Expand Up @@ -79,6 +80,7 @@ jobs:
- envoy.base.utils
- envoy.base.runner
- envoy.base.checker
- envoy.github.abstract
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
Expand Down
5 changes: 5 additions & 0 deletions envoy.github.abstract/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

envoy.github.abstract
=====================

Abstract interfaces for the Github release tool used in Envoy proxy CI
1 change: 1 addition & 0 deletions envoy.github.abstract/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0.1
11 changes: 11 additions & 0 deletions envoy.github.abstract/envoy/github/abstract/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

from .abstract import (
AGithubRelease,
AGithubReleaseAssets,
AGithubReleaseManager)


__all__ = (
"AGithubRelease",
"AGithubReleaseAssets",
"AGithubReleaseManager")
335 changes: 335 additions & 0 deletions envoy.github.abstract/envoy/github/abstract/abstract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
import pathlib
from abc import abstractmethod
from functools import cached_property
from typing import (
Any, AsyncGenerator, Awaitable, Coroutine, Dict, Iterable, Iterator, List,
Optional, Pattern, Set, Type, Union)

import verboselogs # type:ignore

import packaging.version

import aiohttp

import gidgethub.abc

import abstracts

from aio.functional import async_property


class AGithubReleaseAssets(metaclass=abstracts.Abstraction):
"""Base class for Github release assets pusher/fetcher"""

@abstractmethod
def __init__(self, release: "AGithubRelease", path: pathlib.Path) -> None:
raise NotImplementedError

@abstractmethod
async def __aiter__(self) -> AsyncGenerator[
Dict[str, Union[str, pathlib.Path]], Awaitable]:
if False:
yield
raise NotImplementedError

@abstractmethod
def __enter__(self) -> "AGithubReleaseAssets":
raise NotImplementedError

@async_property
@abstractmethod
async def assets(self) -> Dict:
"""Github release asset dictionaries"""
raise NotImplementedError

@async_property
@abstractmethod
async def awaitables(
self) -> AsyncGenerator[
Coroutine[
Any, Any, Dict[str, Union[str, pathlib.Path]]], Dict]:
raise NotImplementedError


class AGithubReleaseAssetsFetcher(
AGithubReleaseAssets, metaclass=abstracts.Abstraction):
"""Fetcher of Github release assets"""

@abstractmethod
def __init__(
self,
release: "AGithubRelease",
path: pathlib.Path,
asset_types: Optional[Dict[str, Pattern[str]]] = None,
append: Optional[bool] = False) -> None:
raise NotImplementedError

@cached_property
@abstractmethod
def asset_types(self) -> Dict[str, Pattern[str]]:
"""Patterns for grouping assets"""
raise NotImplementedError

@abstractmethod
def asset_type(self, asset: Dict) -> Optional[str]:
"""Categorization of an asset into an asset type
The default `asset_types` matcher will just match all files.
A dictionary of `re` matchers can be provided, eg:
```
asset_types = dict(
deb=re.compile(".*(\\.deb|\\.changes)$"),
rpm=re.compile(".*\\.rpm$"))
```
"""
raise NotImplementedError

@abstractmethod
async def download(
self,
asset: Dict) -> Dict[str, Union[str, pathlib.Path]]:
"""Download an asset"""
raise NotImplementedError

@abstractmethod
async def save(
self,
asset_type: str,
name: str,
download: aiohttp.ClientResponse) -> Dict[
str, Union[str, pathlib.Path]]:
"""Save an asset of given type to disk"""
raise NotImplementedError


class AGithubReleaseAssetsPusher(
AGithubReleaseAssets, metaclass=abstracts.Abstraction):
"""Pusher of Github release assets"""

@abstractmethod
def artefacts(self) -> Iterator[pathlib.Path]:
"""Iterator of matching (ie release file type) artefacts found in a given
path
"""
raise NotImplementedError

@abstractmethod
async def upload(
self,
artefact: pathlib.Path,
url: str) -> Dict[str, Union[str, pathlib.Path]]:
"""Upload an artefact from a filepath to a given URL"""
raise NotImplementedError


class AGithubRelease(metaclass=abstracts.Abstraction):
"""A Github tagged release version
Provides CRUD operations for a release and its assets, and therefore
can exist already, or be created.
"""

@abstractmethod
def __init__(self, manager: "AGithubReleaseManager", version: str):
raise NotImplementedError

@async_property(cache=True)
@abstractmethod
async def asset_names(self) -> Set[str]:
"""Set of the names of assets for this release version"""
raise NotImplementedError

@async_property(cache=True)
@abstractmethod
async def assets(self) -> Dict:
raise NotImplementedError

@property
@abstractmethod
def fetcher(self) -> Type[AGithubReleaseAssetsFetcher]:
"""An instance of `AGithubReleaseAssetsFetcher` for fetching release
assets.
"""
raise NotImplementedError

@property
@abstractmethod
def github(self) -> gidgethub.abc.GitHubAPI:
raise NotImplementedError

@property
@abstractmethod
def pusher(self) -> Type[AGithubReleaseAssetsPusher]:
"""An instance of `AGithubReleaseAssetsPusher` for pushing release
assets.
"""
raise NotImplementedError

@property
@abstractmethod
def session(self) -> aiohttp.ClientSession:
raise NotImplementedError

@property
@abstractmethod
def version(self) -> str:
raise NotImplementedError

@abstractmethod
async def create(
self,
assets: Optional[List[pathlib.Path]] = None
) -> Dict[str, Union[List[Dict[str, Union[str, pathlib.Path]]], Dict]]:
"""Create this release version and optionally upload provided assets"""
raise NotImplementedError

@abstractmethod
async def delete(self) -> None:
"""Delete this release version"""
raise NotImplementedError

@abstractmethod
def fail(self, message: str) -> str:
raise NotImplementedError

@abstractmethod
async def fetch(
self,
path: pathlib.Path,
asset_types: Optional[Dict[str, Pattern[str]]] = None,
append: Optional[bool] = False) -> Dict[
str,
List[Dict[str, Union[str, pathlib.Path]]]]:
"""Fetch assets for this version, saving either to a directory or
tarball
"""
raise NotImplementedError

@abstractmethod
async def get(self) -> Dict:
"""Get the release information for this Github release."""
raise NotImplementedError

@abstractmethod
async def push(
self, artefacts: Iterable[pathlib.Path]
) -> Dict[str, List[Dict[str, Union[str, pathlib.Path]]]]:
"""Push assets from a list of paths, either directories or tarballs."""
raise NotImplementedError

@async_property(cache=True)
@abstractmethod
async def upload_url(self) -> str:
"""Upload URL for this release version"""
raise NotImplementedError


class AGithubReleaseManager(metaclass=abstracts.Abstraction):
"""This utility wraps the github API to provide the ability to
create and manage releases and release assets.
A github client connection and/or aiohttp session can be provided if you
wish to reuse the client or session.
If you do not provide a session, one will be created and the async
`.close()` method should called after use.
For this reason, instances of this class can be used as an async
contextmanager, and the session will be automatically closed on exit, for
example:
```python
from tools.github.release.manager import GithubReleaseManager
async with GithubReleaseManager(...) as manager:
await manager["1.19.0"].create()
```
"""

@abstractmethod
async def __aenter__(self) -> "AGithubReleaseManager":
raise NotImplementedError

@abstractmethod
async def __aexit__(self, *args) -> None:
raise NotImplementedError

@abstractmethod
def __getitem__(self, version) -> AGithubRelease:
"""Accessor for a specific Github release"""
raise NotImplementedError

@cached_property
@abstractmethod
def github(self) -> gidgethub.abc.GitHubAPI:
"""An instance of the gidgethub GitHubAPI"""
raise NotImplementedError

@async_property
@abstractmethod
async def latest(self) -> Dict[str, packaging.version.Version]:
"""Returns a dictionary of latest minor and patch versions
For example, given the following versions:
1.19.2, 1.19.1, 1.20.3
It would return:
1.19 -> 1.19.2
1.19.1 -> 1.19.1
1.19.2 -> 1.19.2
1.20 -> 1.20.3
1.20.3 -> 1.20.3
"""
raise NotImplementedError

@cached_property
@abstractmethod
def log(self) -> verboselogs.VerboseLogger:
"""A verbose logger"""
raise NotImplementedError

@async_property
@abstractmethod
async def releases(self) -> List[Dict]:
"""List of dictionaries containing information about available releases,
as returned by the Github API
"""
raise NotImplementedError

@cached_property
@abstractmethod
def releases_url(self) -> pathlib.PurePosixPath:
"""Github API releases URL"""
raise NotImplementedError

@cached_property
@abstractmethod
def session(self) -> aiohttp.ClientSession:
"""Aiohttp Client session, also used for Github API client"""
raise NotImplementedError

@abstractmethod
def fail(self, message: str) -> str:
"""Either raise an error or log a warning and return the message,
dependent on the value of `self.continues`.
"""
raise NotImplementedError

@abstractmethod
def format_version(
self,
version: Union[str, packaging.version.Version]) -> str:
"""Formatted version name - eg `1.19.0` -> `v1.19.0`"""
raise NotImplementedError

@abstractmethod
def parse_version(
self, version: str) -> Optional[packaging.version.Version]:
"""Parsed version - eg `v1.19.0` -> `Version(1.19.0)`"""
raise NotImplementedError
Empty file.
Loading

0 comments on commit 40cd200

Please sign in to comment.