Skip to content

Commit baf9695

Browse files
agnersmdegat01
andauthored
Refactoring around add-on store Repository classes (#5990)
* Rename repository fixture to test_repository Also don't remove the built-in repositories. The list was incomplete, and tests don't seem to require that anymore. * Get rid of StoreType The type doesn't have much value, we have constant strings anyways. * Introduce types.py * Use slug to determine which repository urls to return * Simplify BuiltinRepository enum * Mock GitRepo load * Improve URL handling and repository creation logic * Refactor update_repositories * Get rid of get_from_url It is no longer used in production code. * More refactoring * Address pylint * Introduce is_git_based property to Repository class Return all git based URLs, including the Core repository. * Revert "Introduce is_git_based property to Repository class" This reverts commit dfd5ad7. * Fold type.py into const.py Align more with how Supervisor code is typically structured. * Update supervisor/store/__init__.py Co-authored-by: Mike Degatano <[email protected]> * Apply repository remove suggestion * Fix tests --------- Co-authored-by: Mike Degatano <[email protected]>
1 parent 7873c45 commit baf9695

File tree

16 files changed

+247
-235
lines changed

16 files changed

+247
-235
lines changed

supervisor/backups/backup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,5 +931,5 @@ def restore_repositories(self, replace: bool = False) -> Awaitable[None]:
931931
Return a coroutine.
932932
"""
933933
return self.sys_store.update_repositories(
934-
self.repositories, add_with_errors=True, replace=replace
934+
self.repositories, issue_on_error=True, replace=replace
935935
)

supervisor/store/__init__.py

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections.abc import Awaitable
55
import logging
66

7-
from ..const import ATTR_REPOSITORIES, URL_HASSIO_ADDONS
7+
from ..const import ATTR_REPOSITORIES, REPOSITORY_CORE, URL_HASSIO_ADDONS
88
from ..coresys import CoreSys, CoreSysAttributes
99
from ..exceptions import (
1010
StoreError,
@@ -18,14 +18,10 @@
1818
from ..resolution.const import ContextType, IssueType, SuggestionType
1919
from ..utils.common import FileConfiguration
2020
from .addon import AddonStore
21-
from .const import FILE_HASSIO_STORE, StoreType
21+
from .const import FILE_HASSIO_STORE, BuiltinRepository
2222
from .data import StoreData
2323
from .repository import Repository
24-
from .validate import (
25-
BUILTIN_REPOSITORIES,
26-
SCHEMA_STORE_FILE,
27-
ensure_builtin_repositories,
28-
)
24+
from .validate import SCHEMA_STORE_FILE, ensure_builtin_repositories
2925

3026
_LOGGER: logging.Logger = logging.getLogger(__name__)
3127

@@ -56,7 +52,8 @@ def repository_urls(self) -> list[str]:
5652
return [
5753
repository.source
5854
for repository in self.all
59-
if repository.type == StoreType.GIT
55+
if repository.slug
56+
not in {BuiltinRepository.LOCAL.value, BuiltinRepository.CORE.value}
6057
]
6158

6259
def get(self, slug: str) -> Repository:
@@ -65,19 +62,11 @@ def get(self, slug: str) -> Repository:
6562
raise StoreNotFound()
6663
return self.repositories[slug]
6764

68-
def get_from_url(self, url: str) -> Repository:
69-
"""Return Repository with slug."""
70-
for repository in self.all:
71-
if repository.source != url:
72-
continue
73-
return repository
74-
raise StoreNotFound()
75-
7665
async def load(self) -> None:
7766
"""Start up add-on management."""
7867
# Init custom repositories and load add-ons
7968
await self.update_repositories(
80-
self._data[ATTR_REPOSITORIES], add_with_errors=True
69+
self._data[ATTR_REPOSITORIES], issue_on_error=True
8170
)
8271

8372
@Job(
@@ -126,14 +115,14 @@ async def reload(self, repository: Repository | None = None) -> None:
126115
)
127116
async def add_repository(self, url: str, *, persist: bool = True) -> None:
128117
"""Add a repository."""
129-
await self._add_repository(url, persist=persist, add_with_errors=False)
118+
await self._add_repository(url, persist=persist, issue_on_error=False)
130119

131120
async def _add_repository(
132-
self, url: str, *, persist: bool = True, add_with_errors: bool = False
121+
self, url: str, *, persist: bool = True, issue_on_error: bool = False
133122
) -> None:
134123
"""Add a repository."""
135124
if url == URL_HASSIO_ADDONS:
136-
url = StoreType.CORE
125+
url = REPOSITORY_CORE
137126

138127
repository = Repository.create(self.coresys, url)
139128

@@ -145,7 +134,7 @@ async def _add_repository(
145134
await repository.load()
146135
except StoreGitCloneError as err:
147136
_LOGGER.error("Can't retrieve data from %s due to %s", url, err)
148-
if add_with_errors:
137+
if issue_on_error:
149138
self.sys_resolution.create_issue(
150139
IssueType.FATAL_ERROR,
151140
ContextType.STORE,
@@ -158,7 +147,7 @@ async def _add_repository(
158147

159148
except StoreGitError as err:
160149
_LOGGER.error("Can't load data from repository %s due to %s", url, err)
161-
if add_with_errors:
150+
if issue_on_error:
162151
self.sys_resolution.create_issue(
163152
IssueType.FATAL_ERROR,
164153
ContextType.STORE,
@@ -171,7 +160,7 @@ async def _add_repository(
171160

172161
except StoreJobError as err:
173162
_LOGGER.error("Can't add repository %s due to %s", url, err)
174-
if add_with_errors:
163+
if issue_on_error:
175164
self.sys_resolution.create_issue(
176165
IssueType.FATAL_ERROR,
177166
ContextType.STORE,
@@ -184,7 +173,7 @@ async def _add_repository(
184173

185174
else:
186175
if not await repository.validate():
187-
if add_with_errors:
176+
if issue_on_error:
188177
_LOGGER.error("%s is not a valid add-on repository", url)
189178
self.sys_resolution.create_issue(
190179
IssueType.CORRUPT_REPOSITORY,
@@ -213,7 +202,7 @@ async def _add_repository(
213202

214203
async def remove_repository(self, repository: Repository, *, persist: bool = True):
215204
"""Remove a repository."""
216-
if repository.source in BUILTIN_REPOSITORIES:
205+
if repository.is_builtin:
217206
raise StoreInvalidAddonRepo(
218207
"Can't remove built-in repositories!", logger=_LOGGER.error
219208
)
@@ -236,38 +225,54 @@ async def update_repositories(
236225
self,
237226
list_repositories: list[str],
238227
*,
239-
add_with_errors: bool = False,
228+
issue_on_error: bool = False,
240229
replace: bool = True,
241230
):
242-
"""Add a new custom repository."""
243-
new_rep = set(
244-
ensure_builtin_repositories(list_repositories)
245-
if replace
246-
else list_repositories + self.repository_urls
247-
)
248-
old_rep = {repository.source for repository in self.all}
231+
"""Update repositories by adding new ones and removing stale ones."""
232+
current_repositories = {repository.source for repository in self.all}
233+
234+
# Determine changes needed
235+
if replace:
236+
target_repositories = set(ensure_builtin_repositories(list_repositories))
237+
repositories_to_add = target_repositories - current_repositories
238+
else:
239+
# When not replacing, just add the new repositories
240+
repositories_to_add = set(list_repositories) - current_repositories
241+
target_repositories = current_repositories | repositories_to_add
249242

250243
# Add new repositories
251244
add_errors = await asyncio.gather(
252245
*[
253-
self._add_repository(url, persist=False, add_with_errors=True)
254-
if add_with_errors
246+
# Use _add_repository to avoid JobCondition.SUPERVISOR_UPDATED
247+
# to prevent proper loading of repositories on startup.
248+
self._add_repository(url, persist=False, issue_on_error=True)
249+
if issue_on_error
255250
else self.add_repository(url, persist=False)
256-
for url in new_rep - old_rep
251+
for url in repositories_to_add
257252
],
258253
return_exceptions=True,
259254
)
260255

261-
# Delete stale repositories
262-
remove_errors = await asyncio.gather(
263-
*[
264-
self.remove_repository(self.get_from_url(url), persist=False)
265-
for url in old_rep - new_rep - BUILTIN_REPOSITORIES
266-
],
267-
return_exceptions=True,
268-
)
256+
remove_errors: list[BaseException | None] = []
257+
if replace:
258+
# Determine repositories to remove
259+
repositories_to_remove: list[Repository] = [
260+
repository
261+
for repository in self.all
262+
if repository.source not in target_repositories
263+
and not repository.is_builtin
264+
]
265+
266+
# Remove repositories
267+
remove_errors = await asyncio.gather(
268+
*[
269+
self.remove_repository(repository, persist=False)
270+
for repository in repositories_to_remove
271+
],
272+
return_exceptions=True,
273+
)
269274

270-
# Always update data, even there are errors, some changes may have succeeded
275+
# Always update data, even if there are errors, some changes may have succeeded
271276
await self.data.update()
272277
await self._read_addons()
273278

supervisor/store/const.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,39 @@
33
from enum import StrEnum
44
from pathlib import Path
55

6-
from ..const import SUPERVISOR_DATA
6+
from ..const import (
7+
REPOSITORY_CORE,
8+
REPOSITORY_LOCAL,
9+
SUPERVISOR_DATA,
10+
URL_HASSIO_ADDONS,
11+
)
712

813
FILE_HASSIO_STORE = Path(SUPERVISOR_DATA, "store.json")
14+
"""Repository type definitions for the store."""
915

1016

11-
class StoreType(StrEnum):
12-
"""Store Types."""
17+
class BuiltinRepository(StrEnum):
18+
"""All built-in repositories that come pre-configured."""
1319

14-
CORE = "core"
15-
LOCAL = "local"
16-
GIT = "git"
20+
# Local repository (non-git, special handling)
21+
LOCAL = REPOSITORY_LOCAL
22+
23+
# Git-based built-in repositories
24+
CORE = REPOSITORY_CORE
25+
COMMUNITY_ADDONS = "https://github.com/hassio-addons/repository"
26+
ESPHOME = "https://github.com/esphome/home-assistant-addon"
27+
MUSIC_ASSISTANT = "https://github.com/music-assistant/home-assistant-addon"
28+
29+
@property
30+
def git_url(self) -> str:
31+
"""Return the git URL for this repository."""
32+
if self == BuiltinRepository.LOCAL:
33+
raise RuntimeError("Local repository does not have a git URL")
34+
if self == BuiltinRepository.CORE:
35+
return URL_HASSIO_ADDONS
36+
else:
37+
return self.value # For URL-based repos, value is the URL
38+
39+
40+
# All repositories that are considered "built-in" and protected from removal
41+
ALL_BUILTIN_REPOSITORIES = {repo.value for repo in BuiltinRepository}

supervisor/store/data.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
2626
from ..utils.common import find_one_filetype, read_json_or_yaml_file
2727
from ..utils.json import read_json_file
28-
from .const import StoreType
2928
from .utils import extract_hash_from_path
3029
from .validate import SCHEMA_REPOSITORY_CONFIG
3130

@@ -169,7 +168,7 @@ def _get_addons_list() -> list[Path]:
169168
self.sys_resolution.add_unhealthy_reason(
170169
UnhealthyReason.OSERROR_BAD_MESSAGE
171170
)
172-
elif path.stem != StoreType.LOCAL:
171+
elif repository != REPOSITORY_LOCAL:
173172
suggestion = [SuggestionType.EXECUTE_RESET]
174173
self.sys_resolution.create_issue(
175174
IssueType.CORRUPT_REPOSITORY,

0 commit comments

Comments
 (0)