Updated HACS and also fixed Garadget #727

This commit is contained in:
ccostan
2020-04-09 21:29:27 -04:00
parent 51aab60dea
commit e6e0d442e9
65 changed files with 1510 additions and 1047 deletions

20
config/custom_components/hacs/repositories/__init__.py Executable file → Normal file
View File

@@ -1,6 +1,16 @@
"""Initialize repositories."""
from .theme import HacsTheme
from .integration import HacsIntegration
from .python_script import HacsPythonScript
from .appdaemon import HacsAppdaemon
from .plugin import HacsPlugin
from custom_components.hacs.repositories.theme import HacsTheme
from custom_components.hacs.repositories.integration import HacsIntegration
from custom_components.hacs.repositories.python_script import HacsPythonScript
from custom_components.hacs.repositories.appdaemon import HacsAppdaemon
from custom_components.hacs.repositories.netdaemon import HacsNetdaemon
from custom_components.hacs.repositories.plugin import HacsPlugin
RERPOSITORY_CLASSES = {
"theme": HacsTheme,
"integration": HacsIntegration,
"python_script": HacsPythonScript,
"appdaemon": HacsAppdaemon,
"netdaemon": HacsNetdaemon,
"plugin": HacsPlugin,
}

30
config/custom_components/hacs/repositories/appdaemon.py Executable file → Normal file
View File

@@ -1,27 +1,27 @@
"""Class for appdaemon apps in HACS."""
from aiogithubapi import AIOGitHubException
from .repository import HacsRepository, register_repository_class
from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException
@register_repository_class
class HacsAppdaemon(HacsRepository):
"""Appdaemon apps in HACS."""
category = "appdaemon"
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.information.full_name = full_name
self.information.category = self.category
self.data.full_name = full_name
self.data.category = "appdaemon"
self.content.path.local = self.localpath
self.content.path.remote = "apps"
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
@property
def localpath(self):
"""Return localpath."""
return f"{self.system.config_path}/appdaemon/apps/{self.information.name}"
return f"{self.hacs.system.config_path}/appdaemon/apps/{self.data.name}"
async def validate_repository(self):
"""Validate."""
@@ -39,19 +39,14 @@ class HacsAppdaemon(HacsRepository):
self.validate.errors.append("Repostitory structure not compliant")
self.content.path.remote = addir[0].path
self.information.name = addir[0].name
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.system.status.startup:
if not self.hacs.system.status.startup:
self.logger.error(error)
return self.validate.success
@@ -68,13 +63,13 @@ class HacsAppdaemon(HacsRepository):
async def update_repository(self):
"""Update."""
if self.github.ratelimits.remaining == 0:
if self.hacs.github.ratelimits.remaining == 0:
return
await self.common_update()
# Get appdaemon objects.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
if self.data.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "apps":
@@ -82,14 +77,9 @@ class HacsAppdaemon(HacsRepository):
self.content.path.remote, self.ref
)
self.content.path.remote = addir[0].path
self.information.name = addir[0].name
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Set local path
self.content.path.local = self.localpath

134
config/custom_components/hacs/repositories/integration.py Executable file → Normal file
View File

@@ -1,80 +1,56 @@
"""Class for integrations in HACS."""
import json
from aiogithubapi import AIOGitHubException
from integrationhelper import Logger
from homeassistant.loader import async_get_custom_components
from .repository import HacsRepository, register_repository_class
from ..hacsbase.exceptions import HacsException
from custom_components.hacs.hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.filters import get_first_directory_in_directory
from custom_components.hacs.helpers.information import get_integration_manifest
from custom_components.hacs.repositories.repository import HacsRepository
@register_repository_class
class HacsIntegration(HacsRepository):
"""Integrations in HACS."""
category = "integration"
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.information.full_name = full_name
self.information.category = self.category
self.domain = None
self.data.full_name = full_name
self.data.category = "integration"
self.content.path.remote = "custom_components"
self.content.path.local = self.localpath
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
@property
def localpath(self):
"""Return localpath."""
return f"{self.system.config_path}/custom_components/{self.domain}"
return f"{self.hacs.system.config_path}/custom_components/{self.data.domain}"
async def validate_repository(self):
"""Validate."""
await self.common_validate()
# Attach repository
if self.repository_object is None:
self.repository_object = await self.github.get_repo(
self.information.full_name
)
# Custom step 1: Validate content.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
if self.data.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "custom_components":
try:
ccdir = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
except AIOGitHubException:
name = get_first_directory_in_directory(self.tree, "custom_components")
if name is None:
raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
)
self.content.path.remote = f"custom_components/{name}"
for item in ccdir or []:
if item.type == "dir":
self.content.path.remote = item.path
break
if self.repository_manifest.zip_release:
self.content.objects = self.releases.last_release_object.assets
else:
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects or []:
self.content.files.append(filename.name)
if not await self.get_manifest():
self.validate.errors.append("Missing manifest file.")
try:
await get_integration_manifest(self)
except HacsException as exception:
self.logger.error(exception)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.system.status.startup:
if not self.hacs.system.status.startup:
self.logger.error(error)
return self.validate.success
@@ -86,46 +62,26 @@ class HacsIntegration(HacsRepository):
# Run common registration steps.
await self.common_registration()
# Get the content of the manifest file.
await self.get_manifest()
# Set local path
self.content.path.local = self.localpath
async def update_repository(self):
"""Update."""
if self.github.ratelimits.remaining == 0:
if self.hacs.github.ratelimits.remaining == 0:
return
await self.common_update()
# Get integration objects.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
if self.data.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "custom_components":
ccdir = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
if not isinstance(ccdir, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.path.remote = ccdir[0].path
name = get_first_directory_in_directory(self.tree, "custom_components")
self.content.path.remote = f"custom_components/{name}"
try:
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
except AIOGitHubException:
return
self.content.files = []
if isinstance(self.content.objects, list):
for filename in self.content.objects or []:
self.content.files.append(filename.name)
await self.get_manifest()
await get_integration_manifest(self)
except HacsException as exception:
self.logger.error(exception)
# Set local path
self.content.path.local = self.localpath
@@ -133,33 +89,5 @@ class HacsIntegration(HacsRepository):
async def reload_custom_components(self):
"""Reload custom_components (and config flows)in HA."""
self.logger.info("Reloading custom_component cache")
del self.hass.data["custom_components"]
await async_get_custom_components(self.hass)
async def get_manifest(self):
"""Get info from the manifest file."""
manifest_path = f"{self.content.path.remote}/manifest.json"
try:
manifest = await self.repository_object.get_contents(
manifest_path, self.ref
)
manifest = json.loads(manifest.content)
except Exception: # pylint: disable=broad-except
return False
if manifest:
try:
self.manifest = manifest
self.information.authors = manifest["codeowners"]
self.domain = manifest["domain"]
self.information.name = manifest["name"]
self.information.homeassistant_version = manifest.get("homeassistant")
# Set local path
self.content.path.local = self.localpath
return True
except KeyError as exception:
raise HacsException(
f"Missing expected key {exception} in 'manifest.json'"
)
return False
del self.hacs.hass.data["custom_components"]
await async_get_custom_components(self.hacs.hass)

0
config/custom_components/hacs/repositories/manifest.py Executable file → Normal file
View File

View File

@@ -0,0 +1,90 @@
"""Class for netdaemon apps in HACS."""
from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.filters import get_first_directory_in_directory
class HacsNetdaemon(HacsRepository):
"""Netdaemon apps in HACS."""
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.data.full_name = full_name
self.data.category = "netdaemon"
self.content.path.local = self.localpath
self.content.path.remote = "apps"
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
@property
def localpath(self):
"""Return localpath."""
return f"{self.hacs.system.config_path}/netdaemon/apps/{self.data.name}"
async def validate_repository(self):
"""Validate."""
await self.common_validate()
# Custom step 1: Validate content.
if self.repository_manifest:
if self.data.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "apps":
self.data.domain = get_first_directory_in_directory(
self.tree, self.content.path.remote
)
self.content.path.remote = f"apps/{self.data.name}"
compliant = False
for treefile in self.treefiles:
if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(
".cs"
):
compliant = True
break
if not compliant:
raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.hacs.system.status.startup:
self.logger.error(error)
return self.validate.success
async def registration(self):
"""Registration."""
if not await self.validate_repository():
return False
# Run common registration steps.
await self.common_registration()
# Set local path
self.content.path.local = self.localpath
async def update_repository(self):
"""Update."""
if self.hacs.github.ratelimits.remaining == 0:
return
await self.common_update()
# Get appdaemon objects.
if self.repository_manifest:
if self.data.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "apps":
self.data.domain = get_first_directory_in_directory(
self.tree, self.content.path.remote
)
self.content.path.remote = f"apps/{self.data.name}"
# Set local path
self.content.path.local = self.localpath

95
config/custom_components/hacs/repositories/plugin.py Executable file → Normal file
View File

@@ -1,26 +1,27 @@
"""Class for plugins in HACS."""
import json
from aiogithubapi import AIOGitHubException
from .repository import HacsRepository, register_repository_class
from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.information import find_file_name
@register_repository_class
class HacsPlugin(HacsRepository):
"""Plugins in HACS."""
category = "plugin"
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.information.full_name = full_name
self.information.category = self.category
self.information.file_name = None
self.data.full_name = full_name
self.data.file_name = None
self.data.category = "plugin"
self.information.javascript_type = None
self.content.path.local = (
f"{self.system.config_path}/www/community/{full_name.split('/')[-1]}"
f"{self.hacs.system.config_path}/www/community/{full_name.split('/')[-1]}"
)
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
async def validate_repository(self):
"""Validate."""
@@ -28,7 +29,7 @@ class HacsPlugin(HacsRepository):
await self.common_validate()
# Custom step 1: Validate content.
await self.get_plugin_location()
find_file_name(self)
if self.content.path.remote is None:
raise HacsException(
@@ -38,14 +39,10 @@ class HacsPlugin(HacsRepository):
if self.content.path.remote == "release":
self.content.single = True
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.system.status.startup:
if not self.hacs.system.status.startup:
self.logger.error(error)
return self.validate.success
@@ -59,13 +56,13 @@ class HacsPlugin(HacsRepository):
async def update_repository(self):
"""Update."""
if self.github.ratelimits.remaining == 0:
if self.hacs.github.ratelimits.remaining == 0:
return
# Run common update steps.
await self.common_update()
# Get plugin objects.
await self.get_plugin_location()
find_file_name(self)
# Get JS type
await self.parse_readme_for_jstype()
@@ -76,68 +73,6 @@ class HacsPlugin(HacsRepository):
if self.content.path.remote == "release":
self.content.single = True
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
async def get_plugin_location(self):
"""Get plugin location."""
if self.content.path.remote is not None:
return
possible_locations = ["dist", "release", ""]
if self.repository_manifest:
if self.repository_manifest.content_in_root:
possible_locations = [""]
for location in possible_locations:
if self.content.path.remote is not None:
continue
try:
objects = []
files = []
if location != "release":
try:
objects = await self.repository_object.get_contents(
location, self.ref
)
except AIOGitHubException:
continue
else:
await self.get_releases()
if self.releases.releases:
if self.releases.last_release_object.assets is not None:
objects = self.releases.last_release_object.assets
for item in objects:
if item.name.endswith(".js"):
files.append(item.name)
# Handler for plug requirement 3
valid_filenames = [
f"{self.information.name.replace('lovelace-', '')}.js",
f"{self.information.name}.js",
f"{self.information.name}.umd.js",
f"{self.information.name}-bundle.js",
]
if self.repository_manifest:
if self.repository_manifest.filename:
valid_filenames.append(self.repository_manifest.filename)
for name in valid_filenames:
if name in files:
# YES! We got it!
self.information.file_name = name
self.content.path.remote = location
self.content.objects = objects
self.content.files = files
break
except SystemError:
pass
async def get_package_content(self):
"""Get package content."""
try:
@@ -145,7 +80,7 @@ class HacsPlugin(HacsRepository):
package = json.loads(package.content)
if package:
self.information.authors = package["author"]
self.data.authors = package["author"]
except Exception: # pylint: disable=broad-except
pass

View File

@@ -1,10 +1,11 @@
"""Class for python_scripts in HACS."""
from aiogithubapi import AIOGitHubException
from .repository import HacsRepository, register_repository_class
from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException
from ..helpers.information import find_file_name
@register_repository_class
class HacsPythonScript(HacsRepository):
"""python_scripts in HACS."""
@@ -13,11 +14,12 @@ class HacsPythonScript(HacsRepository):
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.information.full_name = full_name
self.information.category = self.category
self.data.full_name = full_name
self.data.category = "python_script"
self.content.path.remote = "python_scripts"
self.content.path.local = f"{self.system.config_path}/python_scripts"
self.content.path.local = f"{self.hacs.system.config_path}/python_scripts"
self.content.single = True
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
async def validate_repository(self):
"""Validate."""
@@ -25,26 +27,25 @@ class HacsPythonScript(HacsRepository):
await self.common_validate()
# Custom step 1: Validate content.
try:
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
except AIOGitHubException:
if self.data.content_in_root:
self.content.path.remote = ""
compliant = False
for treefile in self.treefiles:
if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(
".py"
):
compliant = True
break
if not compliant:
raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
)
if not isinstance(self.content.objects, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.system.status.startup:
if not self.hacs.system.status.startup:
self.logger.error(error)
return self.validate.success
@@ -57,31 +58,30 @@ class HacsPythonScript(HacsRepository):
await self.common_registration()
# Set name
self.information.name = self.content.objects[0].name.replace(".py", "")
find_file_name(self)
async def update_repository(self): # lgtm[py/similar-function]
"""Update."""
if self.github.ratelimits.remaining == 0:
if self.hacs.github.ratelimits.remaining == 0:
return
# Run common update steps.
await self.common_update()
# Get python_script objects.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
if self.data.content_in_root:
self.content.path.remote = ""
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
compliant = False
for treefile in self.treefiles:
if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(
".py"
):
compliant = True
break
if not compliant:
raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
)
# Update name
self.information.name = self.content.objects[0].name.replace(".py", "")
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
find_file_name(self)

View File

@@ -0,0 +1,17 @@
"""Object for removed repositories."""
import attr
@attr.s(auto_attribs=True)
class RemovedRepository:
repository: str = None
reason: str = None
link: str = None
removal_type: str = None # archived, not_compliant, critical, dev, broken
acknowledged: bool = False
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if key in self.__dict__:
setattr(self, key, data[key])

307
config/custom_components/hacs/repositories/repository.py Executable file → Normal file
View File

@@ -1,29 +1,27 @@
"""Repository."""
# pylint: disable=broad-except, bad-continuation, no-member
import pathlib
import json
import os
import tempfile
import zipfile
from integrationhelper import Validate, Logger
from integrationhelper import Validate
from aiogithubapi import AIOGitHubException
from .manifest import HacsManifest
from ..helpers.misc import get_repository_name
from ..hacsbase import Hacs
from ..hacsbase.exceptions import HacsException
from ..hacsbase.backup import Backup
from ..handler.download import async_download_file, async_save_file
from ..helpers.misc import version_left_higher_then_right
from ..helpers.install import install_repository, version_to_install
RERPOSITORY_CLASSES = {}
def register_repository_class(cls):
"""Register class."""
RERPOSITORY_CLASSES[cls.category] = cls
return cls
from custom_components.hacs.globals import get_hacs
from custom_components.hacs.helpers.information import (
get_info_md_content,
get_repository,
)
from custom_components.hacs.helpers.validate_repository import (
common_validate,
common_update_data,
)
from custom_components.hacs.repositories.repositorydata import RepositoryData
class RepositoryVersions:
@@ -79,6 +77,7 @@ class RepositoryReleases:
published_tags = []
objects = []
releases = False
downloads = None
class RepositoryPath:
@@ -97,26 +96,25 @@ class RepositoryContent:
single = False
class HacsRepository(Hacs):
class HacsRepository:
"""HacsRepository."""
def __init__(self):
"""Set up HacsRepository."""
self.data = {}
self.hacs = get_hacs()
self.data = RepositoryData()
self.content = RepositoryContent()
self.content.path = RepositoryPath()
self.information = RepositoryInformation()
self.repository_object = None
self.status = RepositoryStatus()
self.state = None
self.manifest = {}
self.integration_manifest = {}
self.repository_manifest = HacsManifest.from_dict({})
self.validate = Validate()
self.releases = RepositoryReleases()
self.versions = RepositoryVersions()
self.pending_restart = False
self.logger = None
self.tree = []
self.treefiles = []
self.ref = None
@@ -126,7 +124,7 @@ class HacsRepository(Hacs):
"""Return pending upgrade."""
if self.status.installed:
if self.status.selected_tag is not None:
if self.status.selected_tag == self.information.default_branch:
if self.status.selected_tag == self.data.default_branch:
if self.versions.installed_commit != self.versions.available_commit:
return True
return False
@@ -137,23 +135,20 @@ class HacsRepository(Hacs):
@property
def config_flow(self):
"""Return bool if integration has config_flow."""
if self.manifest:
if self.information.full_name == "hacs/integration":
if self.integration_manifest:
if self.data.full_name == "hacs/integration":
return False
return self.manifest.get("config_flow", False)
return self.integration_manifest.get("config_flow", False)
return False
@property
def custom(self):
"""Return flag if the repository is custom."""
if self.information.full_name.split("/")[0] in [
"custom-components",
"custom-cards",
]:
if self.data.full_name.split("/")[0] in ["custom-components", "custom-cards"]:
return False
if self.information.full_name in self.common.default:
if self.data.full_name.lower() in [x.lower() for x in self.hacs.common.default]:
return False
if self.information.full_name == "hacs/integration":
if self.data.full_name == "hacs/integration":
return False
return True
@@ -164,24 +159,21 @@ class HacsRepository(Hacs):
if self.information.homeassistant_version is not None:
target = self.information.homeassistant_version
if self.repository_manifest is not None:
if self.repository_manifest.homeassistant is not None:
target = self.repository_manifest.homeassistant
if self.data.homeassistant is not None:
target = self.data.homeassistant
if target is not None:
if self.releases.releases:
if not version_left_higher_then_right(self.system.ha_version, target):
if not version_left_higher_then_right(
self.hacs.system.ha_version, target
):
return False
return True
@property
def display_name(self):
"""Return display name."""
return get_repository_name(
self.repository_manifest,
self.information.name,
self.information.category,
self.manifest,
)
return get_repository_name(self)
@property
def display_status(self):
@@ -257,127 +249,41 @@ class HacsRepository(Hacs):
async def common_validate(self):
"""Common validation steps of the repository."""
# Attach helpers
self.validate.errors = []
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
if self.ref is None:
self.ref = version_to_install(self)
# Step 1: Make sure the repository exist.
self.logger.debug("Checking repository.")
try:
self.repository_object = await self.github.get_repo(
self.information.full_name
)
self.data = self.repository_object.attributes
except Exception as exception: # Gotta Catch 'Em All
if not self.system.status.startup:
self.logger.error(exception)
self.validate.errors.append("Repository does not exist.")
return
if not self.tree:
self.tree = await self.repository_object.get_tree(self.ref)
self.treefiles = []
for treefile in self.tree:
self.treefiles.append(treefile.full_path)
# Step 2: Make sure the repository is not archived.
if self.repository_object.archived:
self.validate.errors.append("Repository is archived.")
return
# Step 3: Make sure the repository is not in the blacklist.
if self.information.full_name in self.common.blacklist:
self.validate.errors.append("Repository is in the blacklist.")
return
# Step 4: default branch
self.information.default_branch = self.repository_object.default_branch
# Step 5: Get releases.
await self.get_releases()
# Step 6: Get the content of hacs.json
await self.get_repository_manifest_content()
# Set repository name
self.information.name = self.information.full_name.split("/")[1]
await common_validate(self)
async def common_registration(self):
"""Common registration steps of the repository."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
# Attach repository
if self.repository_object is None:
self.repository_object = await self.github.get_repo(
self.information.full_name
self.repository_object = await get_repository(
self.hacs.session, self.hacs.configuration.token, self.data.full_name
)
self.data.update_data(self.repository_object.attributes)
# Set id
self.information.uid = str(self.repository_object.id)
self.information.uid = str(self.data.id)
# Set topics
self.information.topics = self.repository_object.topics
self.data.topics = self.data.topics
# Set stargazers_count
self.information.stars = self.repository_object.attributes.get(
"stargazers_count", 0
)
self.data.stargazers_count = self.data.stargazers_count
# Set description
if self.repository_object.description:
self.information.description = self.repository_object.description
self.data.description = self.data.description
async def common_update(self):
"""Common information update steps of the repository."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
self.logger.debug("Getting repository information")
# Set ref
if self.ref is None:
self.ref = version_to_install(self)
# Attach repository
self.repository_object = await self.github.get_repo(self.information.full_name)
# Update tree
self.tree = await self.repository_object.get_tree(self.ref)
self.treefiles = []
for treefile in self.tree:
self.treefiles.append(treefile.full_path)
# Update description
if self.repository_object.description:
self.information.description = self.repository_object.description
# Set stargazers_count
self.information.stars = self.repository_object.attributes.get(
"stargazers_count", 0
)
# Update default branch
self.information.default_branch = self.repository_object.default_branch
await common_update_data(self)
# Update last updaeted
self.information.last_updated = self.repository_object.attributes.get(
"pushed_at", 0
)
# Update topics
self.information.topics = self.repository_object.topics
# Update last available commit
await self.repository_object.set_last_commit()
self.versions.available_commit = self.repository_object.last_commit
@@ -386,10 +292,7 @@ class HacsRepository(Hacs):
await self.get_repository_manifest_content()
# Update "info.md"
await self.get_info_md_content()
# Update releases
await self.get_releases()
self.information.additional_info = await get_info_md_content(self)
async def install(self):
"""Common installation steps of the repository."""
@@ -409,18 +312,17 @@ class HacsRepository(Hacs):
return validate
for content in contents or []:
filecontent = await async_download_file(self.hass, content.download_url)
filecontent = await async_download_file(content.download_url)
if filecontent is None:
validate.errors.append(f"[{content.name}] was not downloaded.")
continue
result = await async_save_file(
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}",
filecontent,
f"{tempfile.gettempdir()}/{self.data.filename}", filecontent
)
with zipfile.ZipFile(
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", "r"
f"{tempfile.gettempdir()}/{self.data.filename}", "r"
) as zip_file:
zip_file.extractall(self.content.path.local)
@@ -437,11 +339,13 @@ class HacsRepository(Hacs):
"""Download the content of a directory."""
from custom_components.hacs.helpers.download import download_content
validate = await download_content(self, validate, local_directory)
validate = await download_content(self)
return validate
async def get_repository_manifest_content(self):
"""Get the content of the hacs.json file."""
if not "hacs.json" in [x.filename for x in self.tree]:
return
if self.ref is None:
self.ref = version_to_install(self)
try:
@@ -449,125 +353,44 @@ class HacsRepository(Hacs):
self.repository_manifest = HacsManifest.from_dict(
json.loads(manifest.content)
)
self.data.update_data(json.loads(manifest.content))
except (AIOGitHubException, Exception): # Gotta Catch 'Em All
pass
async def get_info_md_content(self):
"""Get the content of info.md"""
from ..handler.template import render_template
if self.ref is None:
self.ref = version_to_install(self)
info = None
info_files = ["info", "info.md"]
if self.repository_manifest is not None:
if self.repository_manifest.render_readme:
info_files = ["readme", "readme.md"]
try:
root = await self.repository_object.get_contents("", self.ref)
for file in root:
if file.name.lower() in info_files:
info = await self.repository_object.get_contents(
file.name, self.ref
)
break
if info is None:
self.information.additional_info = ""
else:
info = info.content.replace("<svg", "<disabled").replace(
"</svg", "</disabled"
)
self.information.additional_info = render_template(info, self)
except (AIOGitHubException, Exception):
self.information.additional_info = ""
async def get_releases(self):
"""Get repository releases."""
if self.status.show_beta:
self.releases.objects = await self.repository_object.get_releases(
prerelease=True, returnlimit=self.configuration.release_limit
)
else:
self.releases.objects = await self.repository_object.get_releases(
prerelease=False, returnlimit=self.configuration.release_limit
)
if not self.releases.objects:
return
self.releases.releases = True
self.releases.published_tags = []
for release in self.releases.objects:
self.releases.published_tags.append(release.tag_name)
self.releases.last_release_object = self.releases.objects[0]
if self.status.selected_tag is not None:
if self.status.selected_tag != self.information.default_branch:
for release in self.releases.objects:
if release.tag_name == self.status.selected_tag:
self.releases.last_release_object = release
break
if self.releases.last_release_object.assets:
self.releases.last_release_object_downloads = self.releases.last_release_object.assets[
0
].attributes.get(
"download_count"
)
self.versions.available = self.releases.objects[0].tag_name
def remove(self):
"""Run remove tasks."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
self.logger.info("Starting removal")
if self.information.uid in self.common.installed:
self.common.installed.remove(self.information.uid)
for repository in self.repositories:
if self.information.uid in self.hacs.common.installed:
self.hacs.common.installed.remove(self.information.uid)
for repository in self.hacs.repositories:
if repository.information.uid == self.information.uid:
self.repositories.remove(repository)
self.hacs.repositories.remove(repository)
async def uninstall(self):
"""Run uninstall tasks."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
self.logger.info("Uninstalling")
await self.remove_local_directory()
self.status.installed = False
if self.information.category == "integration":
if self.data.category == "integration":
if self.config_flow:
await self.reload_custom_components()
else:
self.pending_restart = True
elif self.information.category == "theme":
elif self.data.category == "theme":
try:
await self.hass.services.async_call("frontend", "reload_themes", {})
await self.hacs.hass.services.async_call(
"frontend", "reload_themes", {}
)
except Exception: # pylint: disable=broad-except
pass
if self.information.full_name in self.common.installed:
self.common.installed.remove(self.information.full_name)
if self.data.full_name in self.hacs.common.installed:
self.hacs.common.installed.remove(self.data.full_name)
self.versions.installed = None
self.versions.installed_commit = None
self.hass.bus.async_fire(
self.hacs.hass.bus.async_fire(
"hacs/repository",
{
"id": 1337,
"action": "uninstall",
"repository": self.information.full_name,
},
{"id": 1337, "action": "uninstall", "repository": self.data.full_name},
)
async def remove_local_directory(self):
@@ -576,13 +399,11 @@ class HacsRepository(Hacs):
from asyncio import sleep
try:
if self.information.category == "python_script":
local_path = "{}/{}.py".format(
self.content.path.local, self.information.name
)
elif self.information.category == "theme":
if self.data.category == "python_script":
local_path = "{}/{}.py".format(self.content.path.local, self.data.name)
elif self.data.category == "theme":
local_path = "{}/{}.yaml".format(
self.content.path.local, self.information.name
self.content.path.local, self.data.name
)
else:
local_path = self.content.path.local
@@ -590,7 +411,7 @@ class HacsRepository(Hacs):
if os.path.exists(local_path):
self.logger.debug(f"Removing {local_path}")
if self.information.category in ["python_script", "theme"]:
if self.data.category in ["python_script", "theme"]:
os.remove(local_path)
else:
shutil.rmtree(local_path)

View File

@@ -0,0 +1,82 @@
"""Repository data."""
from datetime import datetime
from typing import List
import attr
@attr.s(auto_attribs=True)
class RepositoryData:
"""RepositoryData class."""
id: int = 0
full_name: str = ""
pushed_at: str = ""
category: str = ""
archived: bool = False
description: str = ""
manifest_name: str = None
topics: List[str] = []
fork: bool = False
domain: str = ""
default_branch: str = None
stargazers_count: int = 0
last_commit: str = ""
file_name: str = ""
content_in_root: bool = False
zip_release: bool = False
filename: str = ""
render_readme: bool = False
hide_default_branch: bool = False
domains: List[str] = []
country: List[str] = []
authors: List[str] = []
homeassistant: str = None # Minimum Home Assistant version
hacs: str = None # Minimum HACS version
persistent_directory: str = None
iot_class: str = None
@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 self.__dict__
@staticmethod
def create_from_dict(source: dict):
"""Set attributes from dicts."""
data = RepositoryData()
for key in source:
if key in data.__dict__:
if key == "pushed_at":
setattr(
data, key, datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%SZ")
)
elif key == "county":
if isinstance(source[key], str):
setattr(data, key, [source[key]])
else:
setattr(data, key, source[key])
else:
setattr(data, key, source[key])
return data
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if key in self.__dict__:
if key == "pushed_at":
setattr(
self, key, datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%SZ")
)
elif key == "county":
if isinstance(data[key], str):
setattr(self, key, [data[key]])
else:
setattr(self, key, data[key])
else:
setattr(self, key, data[key])

72
config/custom_components/hacs/repositories/theme.py Executable file → Normal file
View File

@@ -1,23 +1,22 @@
"""Class for themes in HACS."""
from .repository import HacsRepository, register_repository_class
from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException
from ..helpers.filters import filter_content_return_one_of_type, find_first_of_filetype
from ..helpers.information import find_file_name
@register_repository_class
class HacsTheme(HacsRepository):
"""Themes in HACS."""
category = "theme"
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.information.full_name = full_name
self.information.category = self.category
self.data.full_name = full_name
self.data.category = "theme"
self.content.path.remote = "themes"
self.content.path.local = f"{self.system.config_path}/themes"
self.content.path.local = f"{self.hacs.system.config_path}/themes/"
self.content.single = False
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
async def validate_repository(self):
"""Validate."""
@@ -27,7 +26,6 @@ class HacsTheme(HacsRepository):
# Custom step 1: Validate content.
compliant = False
for treefile in self.treefiles:
self.logger.debug(treefile)
if treefile.startswith("themes/") and treefile.endswith(".yaml"):
compliant = True
break
@@ -36,25 +34,13 @@ class HacsTheme(HacsRepository):
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
)
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
if not isinstance(self.content.objects, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.files = filter_content_return_one_of_type(
self.treefiles, "themes", "yaml"
)
if self.data.content_in_root:
self.content.path.remote = ""
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.system.status.startup:
if not self.hacs.system.status.startup:
self.logger.error(error)
return self.validate.success
@@ -67,44 +53,20 @@ class HacsTheme(HacsRepository):
await self.common_registration()
# Set name
if self.repository_manifest.filename is not None:
self.information.file_name = self.repository_manifest.filename
else:
self.information.file_name = find_first_of_filetype(
self.content.files, "yaml"
).split("/")[-1]
self.information.name = self.information.file_name.replace(".yaml", "")
self.content.path.local = (
f"{self.system.config_path}/themes/{self.information.name}"
)
find_file_name(self)
self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"
async def update_repository(self): # lgtm[py/similar-function]
"""Update."""
if self.github.ratelimits.remaining == 0:
if self.hacs.github.ratelimits.remaining == 0:
return
# Run common update steps.
await self.common_update()
# Get theme objects.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = filter_content_return_one_of_type(
self.treefiles, "themes", "yaml"
)
if self.data.content_in_root:
self.content.path.remote = ""
# Update name
if self.repository_manifest.filename is not None:
self.information.file_name = self.repository_manifest.filename
else:
self.information.file_name = find_first_of_filetype(
self.content.files, "yaml"
).split("/")[-1]
self.information.name = self.information.file_name.replace(".yaml", "")
self.content.path.local = (
f"{self.system.config_path}/themes/{self.information.name}"
)
find_file_name(self)
self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"