ledgered 0.6.3.dev2__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/.github/workflows/build_and_tests.yml +6 -0
  2. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/CHANGELOG.md +7 -0
  3. {ledgered-0.6.3.dev2/src/ledgered.egg-info → ledgered-0.7.0}/PKG-INFO +2 -1
  4. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/pyproject.toml +1 -0
  5. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/__version__.py +2 -2
  6. ledgered-0.7.0/src/ledgered/github.py +129 -0
  7. {ledgered-0.6.3.dev2 → ledgered-0.7.0/src/ledgered.egg-info}/PKG-INFO +2 -1
  8. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered.egg-info/SOURCES.txt +4 -0
  9. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered.egg-info/requires.txt +1 -0
  10. ledgered-0.7.0/tests/functional/conftest.py +27 -0
  11. ledgered-0.7.0/tests/functional/test_github.py +51 -0
  12. ledgered-0.7.0/tests/unit/test_github.py +48 -0
  13. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/.github/workflows/codeql-analysis.yml +0 -0
  14. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/.github/workflows/fast-checks.yml +0 -0
  15. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/.gitignore +0 -0
  16. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/LICENSE +0 -0
  17. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/README.md +0 -0
  18. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/doc/binary.md +0 -0
  19. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/doc/manifest.md +0 -0
  20. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/setup.cfg +0 -0
  21. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/__init__.py +0 -0
  22. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/binary.py +0 -0
  23. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/__init__.py +0 -0
  24. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/app.py +0 -0
  25. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/cli.py +0 -0
  26. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/constants.py +0 -0
  27. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/errors.py +0 -0
  28. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/manifest.py +0 -0
  29. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/tests.py +0 -0
  30. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/use_cases.py +0 -0
  31. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/manifest/utils.py +0 -0
  32. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/serializers.py +0 -0
  33. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered/utils/__init__.py +0 -0
  34. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered.egg-info/dependency_links.txt +0 -0
  35. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered.egg-info/entry_points.txt +0 -0
  36. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/src/ledgered.egg-info/top_level.txt +0 -0
  37. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/_data/Cargo.toml +0 -0
  38. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/_data/Makefile +0 -0
  39. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/_data/full_correct.toml +0 -0
  40. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/_data/ledger_app.toml +0 -0
  41. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/_data/minimal.toml +0 -0
  42. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/_data/one_leaf.toml +0 -0
  43. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/functional/__init__.py +0 -0
  44. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/functional/manifest/__init__.py +0 -0
  45. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/functional/manifest/test_cli.py +0 -0
  46. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/__init__.py +0 -0
  47. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/manifest/__init__.py +0 -0
  48. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/manifest/test_app.py +0 -0
  49. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/manifest/test_manifest.py +0 -0
  50. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/manifest/test_tests.py +0 -0
  51. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/manifest/test_use_cases.py +0 -0
  52. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/test_binary.py +0 -0
  53. {ledgered-0.6.3.dev2 → ledgered-0.7.0}/tests/unit/test_serializers.py +0 -0
@@ -43,6 +43,12 @@ jobs:
43
43
  - name: Run tests and generate coverage
44
44
  run: pytest -v --tb=short tests/ --cov ledgered --cov-report xml
45
45
 
46
+ - name: Run unit tests and generate coverage
47
+ run: pytest -v --tb=short tests/unit --cov ledgered --cov-report xml
48
+
49
+ - name: Run functional tests and generate coverage
50
+ run: pytest -v --tb=short tests/functional --cov ledgered --cov-report xml --cov-append --token "${{ secrets.GITHUB_TOKEN }}"
51
+
46
52
  - name: Upload coverage to Codecov
47
53
  uses: codecov/codecov-action@v4
48
54
  with:
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [0.7.0] - 2024-04-12
10
+
11
+ ### Added
12
+
13
+ - Added wrapper around GitHub API to ease manipulating Ledger application repositories
14
+
15
+
9
16
  ## [0.6.3] - 2024-03-26
10
17
 
11
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ledgered
3
- Version: 0.6.3.dev2
3
+ Version: 0.7.0
4
4
  Summary: Python tools, utils, libraries, to be used with Ledger cryptodevices
5
5
  Author-email: Ledger <hello@ledger.fr>
6
6
  License: MIT License
@@ -39,6 +39,7 @@ Description-Content-Type: text/markdown
39
39
  License-File: LICENSE
40
40
  Requires-Dist: toml
41
41
  Requires-Dist: pyelftools
42
+ Requires-Dist: pygithub
42
43
  Provides-Extra: dev
43
44
  Requires-Dist: pytest; extra == "dev"
44
45
  Requires-Dist: pytest-cov; extra == "dev"
@@ -29,6 +29,7 @@ requires-python = ">=3.7"
29
29
  dependencies = [
30
30
  "toml",
31
31
  "pyelftools",
32
+ "pygithub",
32
33
  ]
33
34
 
34
35
  [project.optional-dependencies]
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.6.3.dev2'
16
- __version_tuple__ = version_tuple = (0, 6, 3, 'dev2')
15
+ __version__ = version = '0.7.0'
16
+ __version_tuple__ = version_tuple = (0, 7, 0)
@@ -0,0 +1,129 @@
1
+ from enum import IntEnum, auto
2
+ from github import ContentFile as PyContentFile, Github as PyGithub, Repository as PyRepository
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+ from unittest.mock import patch
6
+
7
+ from ledgered.manifest import MANIFEST_FILE_NAME, Manifest
8
+
9
+ LEDGER_ORG_NAME = "ledgerhq"
10
+
11
+
12
+ class Condition(IntEnum):
13
+ WITH = auto()
14
+ WITHOUT = auto()
15
+ ONLY = auto()
16
+
17
+
18
+ class AppRepository(PyRepository.Repository):
19
+
20
+ def __init__(self, *args, **kwargs) -> None:
21
+ super().__init__(*args, **kwargs)
22
+ self._manifest: Optional[Manifest] = None
23
+ self._makefile: Optional[str] = None
24
+ self._branch: str = self.default_branch
25
+
26
+ @property
27
+ def manifest(self) -> Manifest:
28
+ if self._manifest is None:
29
+ manifest = self.get_contents(MANIFEST_FILE_NAME, ref=self.current_branch)
30
+ # `get_contents` can return a list, but here there can only be one manifest
31
+ assert isinstance(manifest, PyContentFile.ContentFile)
32
+ manifest_content = manifest.decoded_content.decode()
33
+ self._manifest = Manifest.from_string(manifest_content)
34
+ return self._manifest
35
+
36
+ @property
37
+ def makefile_path(self) -> Path:
38
+ location = self.manifest.app.build_directory
39
+ if self.manifest.app.is_rust:
40
+ location /= "Cargo.toml"
41
+ else:
42
+ location /= "Makefile"
43
+ return location
44
+
45
+ @property
46
+ def makefile(self) -> str:
47
+ if self._makefile is None:
48
+ # paths on Windows contain "\" which are not compatible with GitHub remote paths
49
+ makefile = self.get_contents(str(self.makefile_path).replace("\\", "/"),
50
+ ref=self.current_branch)
51
+ # `get_contents` can return a list, but here there can only be one Makefile / Cargo.toml
52
+ assert isinstance(makefile, PyContentFile.ContentFile)
53
+ self._makefile = makefile.decoded_content.decode()
54
+ return self._makefile
55
+
56
+ @property
57
+ def variants(self) -> List[str]:
58
+ variants = []
59
+ for line in self.makefile.splitlines():
60
+ if "VARIANTS" in line:
61
+ variants.extend(line.split(' ')[3:])
62
+ elif "VARIANT_VALUES = " in line:
63
+ variants.extend(line.split(" = ")[1].split(' '))
64
+ return variants
65
+
66
+ @property
67
+ def current_branch(self) -> str:
68
+ return self._branch
69
+
70
+ @current_branch.setter
71
+ def current_branch(self, new_branch: str) -> None:
72
+ self._branch = self.get_branch(new_branch).name
73
+ # invalidating previously fetched info, as they may differ on another branch
74
+ self._manifest = None
75
+ self._makefile = None
76
+
77
+
78
+ class GitHubApps(list):
79
+
80
+ def __init__(self, apps: List[AppRepository]):
81
+ super().__init__([r for r in apps if r.name.startswith("app-")])
82
+
83
+ def filter(self,
84
+ name: Optional[str] = None,
85
+ archived: Condition = Condition.WITH,
86
+ private: Condition = Condition.WITH) -> "GitHubApps":
87
+ new_list = [i for i in self]
88
+ # archived filtering
89
+ if archived == Condition.WITHOUT:
90
+ new_list = [r for r in new_list if not r.archived]
91
+ elif archived == Condition.ONLY:
92
+ new_list = [r for r in new_list if r.archived]
93
+ # private filtering
94
+ if private == Condition.WITHOUT:
95
+ new_list = [r for r in new_list if not r.private]
96
+ elif private == Condition.ONLY:
97
+ new_list = [r for r in new_list if r.private]
98
+ # name filtering
99
+ if name is not None:
100
+ new_list = [r for r in new_list if name.lower() in r.name.lower()]
101
+ return GitHubApps(new_list)
102
+
103
+ def first(self, *args, **kwargs) -> Optional[AppRepository]:
104
+ results = self.filter(*args, **kwargs)
105
+ return results[0] if results else None
106
+
107
+
108
+ class GitHubLedgerHQ(PyGithub):
109
+
110
+ def __init__(self, *args, **kwargs) -> None:
111
+ super().__init__(*args, **kwargs)
112
+ self._org = self.get_organization(LEDGER_ORG_NAME)
113
+ self._apps: Optional[GitHubApps] = None
114
+
115
+ @property
116
+ def apps(self) -> GitHubApps:
117
+ if self._apps is None:
118
+ with patch("github.Repository.Repository", AppRepository):
119
+ self._apps = GitHubApps(self._org.get_repos())
120
+ return self._apps
121
+
122
+ def get_app(self, name) -> AppRepository:
123
+ """
124
+ Fetch a specific application repository on GitHub.
125
+ The name must be exact.
126
+ """
127
+ assert name.startswith("app-"), f"'{name}' is not prefixed with 'app-'!"
128
+ with patch("github.Repository.Repository", AppRepository):
129
+ return self._org.get_repo(name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ledgered
3
- Version: 0.6.3.dev2
3
+ Version: 0.7.0
4
4
  Summary: Python tools, utils, libraries, to be used with Ledger cryptodevices
5
5
  Author-email: Ledger <hello@ledger.fr>
6
6
  License: MIT License
@@ -39,6 +39,7 @@ Description-Content-Type: text/markdown
39
39
  License-File: LICENSE
40
40
  Requires-Dist: toml
41
41
  Requires-Dist: pyelftools
42
+ Requires-Dist: pygithub
42
43
  Provides-Extra: dev
43
44
  Requires-Dist: pytest; extra == "dev"
44
45
  Requires-Dist: pytest-cov; extra == "dev"
@@ -11,6 +11,7 @@ doc/manifest.md
11
11
  src/ledgered/__init__.py
12
12
  src/ledgered/__version__.py
13
13
  src/ledgered/binary.py
14
+ src/ledgered/github.py
14
15
  src/ledgered/serializers.py
15
16
  src/ledgered.egg-info/PKG-INFO
16
17
  src/ledgered.egg-info/SOURCES.txt
@@ -35,10 +36,13 @@ tests/_data/ledger_app.toml
35
36
  tests/_data/minimal.toml
36
37
  tests/_data/one_leaf.toml
37
38
  tests/functional/__init__.py
39
+ tests/functional/conftest.py
40
+ tests/functional/test_github.py
38
41
  tests/functional/manifest/__init__.py
39
42
  tests/functional/manifest/test_cli.py
40
43
  tests/unit/__init__.py
41
44
  tests/unit/test_binary.py
45
+ tests/unit/test_github.py
42
46
  tests/unit/test_serializers.py
43
47
  tests/unit/manifest/__init__.py
44
48
  tests/unit/manifest/test_app.py
@@ -1,5 +1,6 @@
1
1
  toml
2
2
  pyelftools
3
+ pygithub
3
4
 
4
5
  [dev]
5
6
  pytest
@@ -0,0 +1,27 @@
1
+ import pytest
2
+ from github.Auth import Token
3
+ from typing import Optional
4
+
5
+ from ledgered.github import GitHubLedgerHQ
6
+
7
+
8
+ def pytest_addoption(parser):
9
+ parser.addoption("--token", required=False, default=None,
10
+ help="Provide a GitHub token so that functional test won't trigger API "
11
+ "restrictions too fast")
12
+
13
+
14
+ @pytest.fixture(scope="session")
15
+ def token(pytestconfig) -> Optional[Token]:
16
+ token = pytestconfig.getoption("token")
17
+ return None if token is None else Token(token)
18
+
19
+
20
+ @pytest.fixture(scope="session")
21
+ def gh(token: Token):
22
+ return GitHubLedgerHQ() if token is None else GitHubLedgerHQ(auth=token)
23
+
24
+
25
+ @pytest.fixture(scope="session")
26
+ def exchange(gh):
27
+ return gh.get_app("app-exchange")
@@ -0,0 +1,51 @@
1
+ import pytest
2
+ import requests
3
+ from pathlib import Path
4
+ from unittest import TestCase
5
+
6
+ from github.GithubException import GithubException
7
+ from ledgered.github import AppRepository, Condition, GitHubApps, GitHubLedgerHQ
8
+
9
+
10
+ def test_apps(gh):
11
+ assert isinstance(gh.apps, list)
12
+ assert isinstance(gh.apps, GitHubApps)
13
+
14
+
15
+ def test_get_app(gh):
16
+ name = "app-exchange"
17
+ app = gh.get_app(name)
18
+ assert isinstance(app, AppRepository)
19
+ assert app.name == name
20
+
21
+
22
+ def test_exchange_manifest(exchange):
23
+ assert exchange.manifest.app.sdk == "c"
24
+ assert len(exchange.manifest.app.devices) == 4
25
+
26
+
27
+ def test_exchange_makefile_path(exchange):
28
+ assert exchange.makefile_path == Path("./Makefile")
29
+
30
+
31
+ def test_exchange_makefile(exchange):
32
+ makefile = requests.get("https://raw.githubusercontent.com/LedgerHQ/app-exchange/develop/Makefile").content.decode()
33
+ assert exchange.makefile == makefile
34
+
35
+
36
+ def test_exchange_branches(exchange):
37
+ assert exchange.current_branch == "develop"
38
+ exchange.current_branch = "master"
39
+ assert exchange.current_branch == "master"
40
+
41
+ with pytest.raises(GithubException):
42
+ exchange.current_branch = "does not exists"
43
+
44
+
45
+ def test_exchange_variant(exchange):
46
+ assert exchange.variants == ["exchange"]
47
+
48
+
49
+ def test_starknet_makefile_path(gh):
50
+ app = gh.get_app("app-starknet")
51
+ assert app.makefile_path == Path("./Cargo.toml")
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass
2
+ from unittest import TestCase
3
+ from unittest.mock import patch, MagicMock
4
+
5
+ from ledgered.github import AppRepository, Condition, GitHubApps, GitHubLedgerHQ
6
+
7
+
8
+ @dataclass
9
+ class AppRepositoryMock:
10
+ name: str
11
+ archived: bool = False
12
+ private: bool = False
13
+
14
+
15
+ class TestGitHubApps(TestCase):
16
+
17
+ def setUp(self):
18
+ self.app1 = AppRepositoryMock("app-1")
19
+ self.app2 = AppRepositoryMock("not-app")
20
+ self.app3 = AppRepositoryMock("app-3", private=True)
21
+ self.app4 = AppRepositoryMock("app-4", archived=True)
22
+ self.apps = GitHubApps([self.app1, self.app2, self.app3, self.app4])
23
+
24
+ def test___init__(self):
25
+ self.assertListEqual(self.apps, [self.app1, self.app3, self.app4])
26
+
27
+ def test_filter(self):
28
+ self.assertCountEqual(self.apps.filter(), self.apps)
29
+ self.assertCountEqual(self.apps.filter(name="3"), [self.app3])
30
+ self.assertCountEqual(self.apps.filter(name="app"), self.apps)
31
+ self.assertCountEqual(self.apps.filter(archived=Condition.WITHOUT), [self.app1, self.app3])
32
+ self.assertCountEqual(self.apps.filter(archived=Condition.ONLY), [self.app4])
33
+ self.assertCountEqual(self.apps.filter(private=Condition.WITHOUT), [self.app1, self.app4])
34
+ self.assertCountEqual(self.apps.filter(private=Condition.ONLY), [self.app3])
35
+
36
+ def test_first(self):
37
+ self.assertEqual(self.apps.first("3"), self.app3)
38
+ self.assertEqual(self.apps.first(), self.app1)
39
+
40
+
41
+ class TestGitHubLedgerHQ(TestCase):
42
+
43
+ def setUp(self):
44
+ self.g = GitHubLedgerHQ()
45
+
46
+ def test_get_app_wrong_name(self):
47
+ with self.assertRaises(AssertionError):
48
+ self.g.get_app("not-starting-with-app-")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes