"""Repository."""
from __future__ import annotations
from asyncio import sleep
from datetime import datetime
import os
import pathlib
import shutil
import tempfile
from typing import TYPE_CHECKING, Any
import zipfile
from aiogithubapi import (
AIOGitHubAPIException,
AIOGitHubAPINotModifiedException,
GitHubReleaseModel,
)
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
import attr
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from ..const import DOMAIN
from ..enums import ConfigurationType, HacsDispatchEvent, RepositoryFile
from ..exceptions import (
HacsException,
HacsNotModifiedException,
HacsRepositoryArchivedException,
HacsRepositoryExistException,
)
from ..types import DownloadableContent
from ..utils.backup import Backup, BackupNetDaemon
from ..utils.decode import decode_content
from ..utils.decorator import concurrent
from ..utils.filters import filter_content_return_one_of_type
from ..utils.json import json_loads
from ..utils.logger import LOGGER
from ..utils.path import is_safe
from ..utils.queue_manager import QueueManager
from ..utils.store import async_remove_store
from ..utils.template import render_template
from ..utils.url import github_archive, github_release_asset
from ..utils.validate import Validate
from ..utils.version import (
version_left_higher_or_equal_then_right,
version_left_higher_then_right,
)
from ..utils.workarounds import DOMAIN_OVERRIDES
if TYPE_CHECKING:
from ..base import HacsBase
TOPIC_FILTER = (
"add-on",
"addon",
"app",
"appdaemon-apps",
"appdaemon",
"custom-card",
"custom-cards",
"custom-component",
"custom-components",
"customcomponents",
"hacktoberfest",
"hacs-default",
"hacs-integration",
"hacs-repository",
"hacs",
"hass",
"hassio",
"home-assistant-custom",
"home-assistant-frontend",
"home-assistant-hacs",
"home-assistant-sensor",
"home-assistant",
"home-automation",
"homeassistant-components",
"homeassistant-integration",
"homeassistant-sensor",
"homeassistant",
"homeautomation",
"integration",
"lovelace-ui",
"lovelace",
"media-player",
"mediaplayer",
"netdaemon",
"plugin",
"python_script",
"python-script",
"python",
"sensor",
"smart-home",
"smarthome",
"template",
"templates",
"theme",
"themes",
)
REPOSITORY_KEYS_TO_EXPORT = (
# Keys can not be removed from this list until v3
# If keys are added, the action need to be re-run with force
("description", ""),
("downloads", 0),
("domain", None),
("etag_releases", None),
("etag_repository", None),
("full_name", ""),
("last_commit", None),
("last_updated", 0),
("last_version", None),
("manifest_name", None),
("open_issues", 0),
("stargazers_count", 0),
("topics", []),
)
HACS_MANIFEST_KEYS_TO_EXPORT = (
# Keys can not be removed from this list until v3
# If keys are added, the action need to be re-run with force
("country", []),
("name", None),
)
class FileInformation:
"""FileInformation."""
def __init__(self, url, path, name):
self.download_url = url
self.path = path
self.name = name
@attr.s(auto_attribs=True)
class RepositoryData:
"""RepositoryData class."""
archived: bool = False
authors: list[str] = []
category: str = ""
config_flow: bool = False
default_branch: str = None
description: str = ""
domain: str = None
downloads: int = 0
etag_repository: str = None
etag_releases: str = None
file_name: str = ""
first_install: bool = False
full_name: str = ""
hide: bool = False
has_issues: bool = True
id: int = 0
installed_commit: str = None
installed_version: str = None
installed: bool = False
last_commit: str = None
last_fetched: datetime = None
last_updated: str = 0
last_version: str = None
manifest_name: str = None
new: bool = True
open_issues: int = 0
published_tags: list[str] = []
releases: bool = False
selected_tag: str = None
show_beta: bool = False
stargazers_count: int = 0
topics: list[str] = []
@property
def name(self):
"""Return the name."""
if self.category in ["integration", "netdaemon"]:
return self.domain
return self.full_name.split("/")[-1]
def to_json(self):
"""Export to json."""
return attr.asdict(self, filter=lambda attr, value: attr.name != "last_fetched")
@staticmethod
def create_from_dict(source: dict, action: bool = False) -> RepositoryData:
"""Set attributes from dicts."""
data = RepositoryData()
data.update_data(source, action)
return data
def update_data(self, data: dict, action: bool = False) -> None:
"""Update data of the repository."""
for key, value in data.items():
if key not in self.__dict__:
continue
if key == "last_fetched" and isinstance(value, float):
setattr(self, key, datetime.fromtimestamp(value))
elif key == "id":
setattr(self, key, str(value))
elif key == "country":
if isinstance(value, str):
setattr(self, key, [value])
else:
setattr(self, key, value)
elif key == "topics" and not action:
setattr(self, key, [topic for topic in value if topic not in TOPIC_FILTER])
else:
setattr(self, key, value)
@attr.s(auto_attribs=True)
class HacsManifest:
"""HacsManifest class."""
content_in_root: bool = False
country: list[str] = []
filename: str = None
hacs: str = None # Minimum HACS version
hide_default_branch: bool = False
homeassistant: str = None # Minimum Home Assistant version
manifest: dict = {}
name: str = None
persistent_directory: str = None
render_readme: bool = False
zip_release: bool = False
def to_dict(self):
"""Export to json."""
return attr.asdict(self)
@staticmethod
def from_dict(manifest: dict):
"""Set attributes from dicts."""
if manifest is None:
raise HacsException("Missing manifest data")
manifest_data = HacsManifest()
manifest_data.manifest = {
k: v
for k, v in manifest.items()
if k in manifest_data.__dict__ and v != manifest_data.__getattribute__(k)
}
for key, value in manifest_data.manifest.items():
if key == "country" and isinstance(value, str):
setattr(manifest_data, key, [value])
elif key in manifest_data.__dict__:
setattr(manifest_data, key, value)
return manifest_data
def update_data(self, data: dict) -> None:
"""Update the manifest data."""
for key, value in data.items():
if key not in self.__dict__:
continue
if key == "country":
if isinstance(value, str):
setattr(self, key, [value])
else:
setattr(self, key, value)
else:
setattr(self, key, value)
class RepositoryReleases:
"""RepositoyReleases."""
last_release = None
last_release_object = None
published_tags = []
objects: list[GitHubReleaseModel] = []
releases = False
downloads = None
class RepositoryPath:
"""RepositoryPath."""
local: str | None = None
remote: str | None = None
class RepositoryContent:
"""RepositoryContent."""
path: RepositoryPath | None = None
files = []
objects = []
single = False
class HacsRepository:
"""HacsRepository."""
def __init__(self, hacs: HacsBase) -> None:
"""Set up HacsRepository."""