-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[add] Wrapper around GitHub Ledger applications
- Loading branch information
1 parent
64e1a82
commit 3b85fbb
Showing
7 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
from enum import IntEnum, auto | ||
from github import ContentFile as PyContentFile, Github as PyGithub, Repository as PyRepository | ||
from pathlib import Path | ||
from typing import List, Optional | ||
from unittest.mock import patch | ||
|
||
from ledgered.manifest import MANIFEST_FILE_NAME, Manifest | ||
|
||
LEDGER_ORG_NAME = "ledgerhq" | ||
|
||
|
||
class Condition(IntEnum): | ||
WITH = auto() | ||
WITHOUT = auto() | ||
ONLY = auto() | ||
|
||
|
||
class AppRepository(PyRepository.Repository): | ||
|
||
def __init__(self, *args, **kwargs) -> None: | ||
super().__init__(*args, **kwargs) | ||
self._manifest: Optional[Manifest] = None | ||
self._makefile: Optional[str] = None | ||
self._branch: str = self.default_branch | ||
|
||
@property | ||
def manifest(self) -> Manifest: | ||
if self._manifest is None: | ||
manifest = self.get_contents(MANIFEST_FILE_NAME, ref=self.current_branch) | ||
# `get_contents` can return a list, but here there can only be one manifest | ||
assert isinstance(manifest, PyContentFile.ContentFile) | ||
manifest_content = manifest.decoded_content.decode() | ||
self._manifest = Manifest.from_string(manifest_content) | ||
return self._manifest | ||
|
||
@property | ||
def makefile_path(self) -> Path: | ||
location = self.manifest.app.build_directory | ||
if self.manifest.app.is_rust: | ||
location /= "Cargo.toml" | ||
else: | ||
location /= "Makefile" | ||
return location | ||
|
||
@property | ||
def makefile(self) -> str: | ||
if self._makefile is None: | ||
# paths on Windows contain "\" which are not compatible with GitHub remote paths | ||
makefile = self.get_contents(str(self.makefile_path).replace("\\", "/"), | ||
ref=self.current_branch) | ||
# `get_contents` can return a list, but here there can only be one Makefile / Cargo.toml | ||
assert isinstance(makefile, PyContentFile.ContentFile) | ||
self._makefile = makefile.decoded_content.decode() | ||
return self._makefile | ||
|
||
@property | ||
def variants(self) -> List[str]: | ||
variants = [] | ||
for line in self.makefile.splitlines(): | ||
if "VARIANTS" in line: | ||
variants.extend(line.split(' ')[3:]) | ||
elif "VARIANT_VALUES = " in line: | ||
variants.extend(line.split(" = ")[1].split(' ')) | ||
return variants | ||
|
||
@property | ||
def current_branch(self) -> str: | ||
return self._branch | ||
|
||
@current_branch.setter | ||
def current_branch(self, new_branch: str) -> None: | ||
self._branch = self.get_branch(new_branch).name | ||
# invalidating previously fetched info, as they may differ on another branch | ||
self._manifest = None | ||
self._makefile = None | ||
|
||
|
||
class GitHubApps(list): | ||
|
||
def __init__(self, apps: List[AppRepository]): | ||
super().__init__([r for r in apps if r.name.startswith("app-")]) | ||
|
||
def filter(self, | ||
name: Optional[str] = None, | ||
archived: Condition = Condition.WITH, | ||
private: Condition = Condition.WITH) -> "GitHubApps": | ||
new_list = [i for i in self] | ||
# archived filtering | ||
if archived == Condition.WITHOUT: | ||
new_list = [r for r in new_list if not r.archived] | ||
elif archived == Condition.ONLY: | ||
new_list = [r for r in new_list if r.archived] | ||
# private filtering | ||
if private == Condition.WITHOUT: | ||
new_list = [r for r in new_list if not r.private] | ||
elif private == Condition.ONLY: | ||
new_list = [r for r in new_list if r.private] | ||
# name filtering | ||
if name is not None: | ||
new_list = [r for r in new_list if name.lower() in r.name.lower()] | ||
return GitHubApps(new_list) | ||
|
||
def first(self, *args, **kwargs) -> Optional[AppRepository]: | ||
results = self.filter(*args, **kwargs) | ||
return results[0] if results else None | ||
|
||
|
||
class GitHubLedgerHQ(PyGithub): | ||
|
||
def __init__(self, *args, **kwargs) -> None: | ||
super().__init__(*args, **kwargs) | ||
self._org = self.get_organization(LEDGER_ORG_NAME) | ||
self._apps: Optional[GitHubApps] = None | ||
|
||
@property | ||
def apps(self) -> GitHubApps: | ||
if self._apps is None: | ||
with patch("github.Repository.Repository", AppRepository): | ||
self._apps = GitHubApps(self._org.get_repos()) | ||
return self._apps | ||
|
||
def get_app(self, name) -> AppRepository: | ||
""" | ||
Fetch a specific application repository on GitHub. | ||
The name must be exact. | ||
""" | ||
assert name.startswith("app-"), f"'{name}' is not prefixed with 'app-'!" | ||
with patch("github.Repository.Repository", AppRepository): | ||
return self._org.get_repo(name) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import pytest | ||
from github.Auth import Token | ||
from typing import Optional | ||
|
||
from ledgered.github import GitHubLedgerHQ | ||
|
||
|
||
def pytest_addoption(parser): | ||
parser.addoption("--token", required=False, default=None, | ||
help="Provide a GitHub token so that functional test won't trigger API " | ||
"restrictions too fast") | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def token(pytestconfig) -> Optional[Token]: | ||
token = pytestconfig.getoption("token") | ||
return None if token is None else Token(token) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def gh(token: Token): | ||
return GitHubLedgerHQ() if token is None else GitHubLedgerHQ(auth=token) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def exchange(gh): | ||
return gh.get_app("app-exchange") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import pytest | ||
import requests | ||
from pathlib import Path | ||
from unittest import TestCase | ||
|
||
from github.GithubException import GithubException | ||
from ledgered.github import AppRepository, Condition, GitHubApps, GitHubLedgerHQ | ||
|
||
|
||
def test_apps(gh): | ||
assert isinstance(gh.apps, list) | ||
assert isinstance(gh.apps, GitHubApps) | ||
|
||
|
||
def test_get_app(gh): | ||
name = "app-exchange" | ||
app = gh.get_app(name) | ||
assert isinstance(app, AppRepository) | ||
assert app.name == name | ||
|
||
|
||
def test_exchange_manifest(exchange): | ||
assert exchange.manifest.app.sdk == "c" | ||
assert len(exchange.manifest.app.devices) == 4 | ||
|
||
|
||
def test_exchange_makefile_path(exchange): | ||
assert exchange.makefile_path == Path("./Makefile") | ||
|
||
|
||
def test_exchange_makefile(exchange): | ||
makefile = requests.get("https://raw.githubusercontent.com/LedgerHQ/app-exchange/develop/Makefile").content.decode() | ||
assert exchange.makefile == makefile | ||
|
||
|
||
def test_exchange_branches(exchange): | ||
assert exchange.current_branch == "develop" | ||
exchange.current_branch = "master" | ||
assert exchange.current_branch == "master" | ||
|
||
with pytest.raises(GithubException): | ||
exchange.current_branch = "does not exists" | ||
|
||
|
||
def test_exchange_variant(exchange): | ||
assert exchange.variants == ["exchange"] | ||
|
||
|
||
def test_starknet_makefile_path(gh): | ||
app = gh.get_app("app-starknet") | ||
assert app.makefile_path == Path("./Cargo.toml") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from dataclasses import dataclass | ||
from unittest import TestCase | ||
from unittest.mock import patch, MagicMock | ||
|
||
from ledgered.github import AppRepository, Condition, GitHubApps, GitHubLedgerHQ | ||
|
||
|
||
@dataclass | ||
class AppRepositoryMock: | ||
name: str | ||
archived: bool = False | ||
private: bool = False | ||
|
||
|
||
class TestGitHubApps(TestCase): | ||
|
||
def setUp(self): | ||
self.app1 = AppRepositoryMock("app-1") | ||
self.app2 = AppRepositoryMock("not-app") | ||
self.app3 = AppRepositoryMock("app-3", private=True) | ||
self.app4 = AppRepositoryMock("app-4", archived=True) | ||
self.apps = GitHubApps([self.app1, self.app2, self.app3, self.app4]) | ||
|
||
def test___init__(self): | ||
self.assertListEqual(self.apps, [self.app1, self.app3, self.app4]) | ||
|
||
def test_filter(self): | ||
self.assertCountEqual(self.apps.filter(), self.apps) | ||
self.assertCountEqual(self.apps.filter(name="3"), [self.app3]) | ||
self.assertCountEqual(self.apps.filter(name="app"), self.apps) | ||
self.assertCountEqual(self.apps.filter(archived=Condition.WITHOUT), [self.app1, self.app3]) | ||
self.assertCountEqual(self.apps.filter(archived=Condition.ONLY), [self.app4]) | ||
self.assertCountEqual(self.apps.filter(private=Condition.WITHOUT), [self.app1, self.app4]) | ||
self.assertCountEqual(self.apps.filter(private=Condition.ONLY), [self.app3]) | ||
|
||
def test_first(self): | ||
self.assertEqual(self.apps.first("3"), self.app3) | ||
self.assertEqual(self.apps.first(), self.app1) | ||
|
||
|
||
class TestGitHubLedgerHQ(TestCase): | ||
|
||
def setUp(self): | ||
self.g = GitHubLedgerHQ() | ||
|
||
def test_get_app_wrong_name(self): | ||
with self.assertRaises(AssertionError): | ||
self.g.get_app("not-starting-with-app-") |