async def validate_repository(self): """Validate.""" await self.common_validate() # Custom step 1: Validate content. if self.data.content_in_root: self.content.path.remote = "" if self.content.path.remote == "custom_components": 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}" try: await get_integration_manifest(self) except HacsException as exception: if self.hacs.action: raise HacsException(exception) self.logger.error(exception) # 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 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]: if self.hacs.action: raise HacsException( "No hacs.json file in the root of the repository.") return if self.hacs.action: self.logger.info("Found hacs.json") self.ref = version_to_install(self) try: manifest = await self.repository_object.get_contents( "hacs.json", self.ref) self.repository_manifest = HacsManifest.from_dict( json.loads(manifest.content)) self.data.update_data(json.loads(manifest.content)) if self.hacs.action: self.logger.info(json.loads(manifest.content)) except (AIOGitHubAPIException, Exception) as exception: # Gotta Catch 'Em All if self.hacs.action: raise HacsException( f"hacs.json file is not valid ({exception}).") if self.hacs.action: self.logger.info("hacs.json is valid")
async def get_integration_manifest(repository): """Return the integration manifest.""" manifest_path = f"{repository.content.path.remote}/manifest.json" if not manifest_path in [x.full_path for x in repository.tree]: raise HacsException(f"No file found '{manifest_path}'") try: manifest = await repository.repository_object.get_contents( manifest_path, repository.ref) manifest = json.loads(manifest.content) except Exception as exception: # pylint: disable=broad-except raise HacsException(f"Could not read manifest.json [{exception}]") try: repository.manifest = manifest repository.information.authors = manifest["codeowners"] repository.domain = manifest["domain"] repository.information.name = manifest["name"] repository.information.homeassistant_version = manifest.get( "homeassistant") # Set local path repository.content.path.local = repository.localpath except KeyError as exception: raise HacsException( f"Missing expected key {exception} in 'manifest.json'")
async def register_repository(full_name, category, check=True, ref=None, action=False): """Register a repository.""" hacs = get_hacs() hacs.action = action from custom_components.hacs.repositories import ( RERPOSITORY_CLASSES, ) # To hanle import error if full_name in hacs.common.skip: if full_name != "hacs/integration": raise HacsExpectedException(f"Skipping {full_name}") if category not in RERPOSITORY_CLASSES: raise HacsException(f"{category} is not a valid repository category.") repository = RERPOSITORY_CLASSES[category](full_name) if check: try: await repository.registration(ref) if hacs.system.status.new: repository.status.new = False if repository.validate.errors: hacs.common.skip.append(repository.data.full_name) if not hacs.system.status.startup: hacs.logger.error(f"Validation for {full_name} failed.") if hacs.action: raise HacsException(f"Validation for {full_name} failed.") return repository.validate.errors if hacs.action: repository.logger.info("Validation complete") else: repository.logger.info("Registration complete") except AIOGitHubException as exception: hacs.common.skip.append(repository.data.full_name) raise HacsException( f"Validation for {full_name} failed with {exception}.") if hacs.hass is not None: hacs.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "registration", "repository": repository.data.full_name, "repository_id": repository.information.uid, }, ) hacs.repositories.append(repository)
async def uninstall(self): """Run uninstall tasks.""" self.logger.info("Uninstalling") if not await self.remove_local_directory(): raise HacsException("Could not uninstall") self.data.installed = False if self.data.category == "integration": if self.data.config_flow: await self.reload_custom_components() else: self.pending_restart = True elif self.data.category == "theme": try: await self.hacs.hass.services.async_call( "frontend", "reload_themes", {}) except Exception: # pylint: disable=broad-except pass if self.data.full_name in self.hacs.common.installed: self.hacs.common.installed.remove(self.data.full_name) await async_remove_store(self.hacs.hass, f"hacs/{self.data.id}.hacs") self.data.installed_version = None self.data.installed_commit = None self.hacs.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "uninstall", "repository": self.data.full_name }, )
async def common_registration(self): """Common registration steps of the repository.""" # Attach repository if self.repository_object is None: 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.data.id) # Set topics self.data.topics = self.data.topics # Set stargazers_count self.data.stargazers_count = self.data.stargazers_count # Set description self.data.description = self.data.description if self.hacs.action: if self.data.description is None or len( self.data.description) == 0: raise HacsException("Missing repository description")
async def run_action_checks(repository): """Checks to run as an action.""" issues = [] if os.getenv("SKIP_BRANDS_CHECK") is None: brands = await repository.hacs.github.get_repo("home-assistant/brands") brandstree = await get_tree(brands, "master") if repository.integration_manifest["domain"] not in [ x.filename for x in brandstree ]: issues.append(f"Integration not added to {BRANDS_REPO}") else: repository.logger.info( f"Integration is added to {BRANDS_REPO}, nice!") if (repository.integration_manifest.get("requirements") is not None and len(repository.integration_manifest.get("requirements")) != 0): wheels = await repository.hacs.github.get_repo( "home-assistant/wheels-custom-integrations") wheeltree = await get_tree(wheels, "master") wheelfiles = [x.filename for x in wheeltree] if (f"{repository.integration_manifest['domain']}.json" in wheelfiles or repository.integration_manifest["domain"] in wheelfiles): repository.logger.info( f"Integration is added to {WHEEL_REPO}, nice!") else: issues.append(f"Integration not added to {WHEEL_REPO}") if issues: for issue in issues: repository.logger.error(issue) raise HacsException(f"Found issues while validating the repository")
async def get_tree(repository, ref): """Return the repository tree.""" try: tree = await repository.get_tree(ref) return tree except AIOGitHubException as exception: raise HacsException(exception)
async def get_releases(repository, prerelease=False, returnlimit=5): """Return the repository releases.""" try: releases = await repository.get_releases(prerelease, returnlimit) return releases except AIOGitHubException as exception: raise HacsException(exception)
async def load_hacs_repository(): """Load HACS repositroy.""" hacs = get_hacs() try: repository = hacs.get_by_name("hacs/integration") if repository is None: await register_repository("hacs/integration", "integration") repository = hacs.get_by_name("hacs/integration") if repository is None: raise HacsException("Unknown error") repository.data.installed = True repository.data.installed_version = VERSION repository.data.new = False hacs.repo = repository.repository_object hacs.data_repo = await get_repository(hacs.session, hacs.configuration.token, "hacs/default") except HacsException as exception: if "403" in f"{exception}": hacs.logger.critical( "GitHub API is ratelimited, or the token is wrong.") else: hacs.logger.critical(f"[{exception}] - Could not load HACS!") return False return True
async def get_repository(session, token, repository_full_name): """Return a repository object or None.""" try: github = AIOGitHub(token, session) repository = await github.get_repo(repository_full_name) return repository except AIOGitHubException as exception: raise HacsException(exception)
async def run_action_checks(repository): """Checks to run as an action.""" brands = await repository.hacs.github.get_repo("home-assistant/brands") brandstree = await get_tree(brands, "master") if repository.integration_manifest["domain"] not in [ x.filename for x in brandstree ]: raise HacsException(f"Integration not added to {BRANDS_REPO}") repository.logger.info(f"Integration is added to {BRANDS_REPO}, nice!") if (repository.integration_manifest.get("requirements") is not None and len(repository.integration_manifest.get("requirements")) != 0): wheels = await repository.hacs.github.get_repo( "home-assistant/wheels-custom-integrations") wheeltree = await get_tree(wheels, "master") if f"{repository.integration_manifest['domain']}.json" not in [ x.filename for x in wheeltree ]: raise HacsException(f"Integration not added to {WHEEL_REPO}") repository.logger.info(f"Integration is added to {WHEEL_REPO}, nice!")
def from_dict(configuration: dict, options: dict): """Set attributes from dicts.""" if isinstance(options, bool) or isinstance(configuration.get("options"), bool): raise HacsException("Configuration is not valid.") if options is None: options = {} if not configuration: raise HacsException("Configuration is not valid.") config = Configuration() config.config = configuration config.options = options for conf_type in [configuration, options]: for key in conf_type: setattr(config, key, conf_type[key]) return config
def from_dict(manifest: dict): """Set attributes from dicts.""" if manifest is None: raise HacsException("Missing manifest data") manifest_data = HacsManifest() manifest_data.manifest = manifest for key in manifest: setattr(manifest_data, key, manifest[key]) return manifest_data
async def get_integration_manifest(repository): """Return the integration manifest.""" if repository.data.content_in_root: manifest_path = "manifest.json" else: manifest_path = f"{repository.content.path.remote}/manifest.json" if not manifest_path in [x.full_path for x in repository.tree]: raise HacsException(f"No file found '{manifest_path}'") try: manifest = await repository.repository_object.get_contents( manifest_path, repository.ref) manifest = json.loads(manifest.content) except Exception as exception: # pylint: disable=broad-except raise HacsException(f"Could not read manifest.json [{exception}]") try: repository.integration_manifest = manifest repository.data.authors = manifest["codeowners"] repository.data.domain = manifest["domain"] repository.data.manifest_name = manifest["name"] repository.data.config_flow = manifest.get("config_flow", False) if repository.hacs.action: if manifest.get("documentation") is None: raise HacsException("manifest.json is missing documentation") if manifest.get("homeassistant") is not None: raise HacsException( "The homeassistant key in manifest.json is no longer valid" ) # if manifest.get("issue_tracker") is None: # raise HacsException("The 'issue_tracker' is missing in manifest.json") # Set local path repository.content.path.local = repository.localpath except KeyError as exception: raise HacsException( f"Missing expected key {exception} in '{manifest_path}'")
async def download_content(repository): """Download the content of a directory.""" queue = QueueManager() contents = gather_files_to_download(repository) repository.logger.debug(repository.data.filename) if not contents: raise HacsException("No content to download") for content in contents: if repository.data.content_in_root and repository.data.filename: if content.name != repository.data.filename: continue queue.add(dowload_repository_content(repository, content)) await queue.execute()
async def download_content(repository, validate, local_directory): """Download the content of a directory.""" contents = gather_files_to_download(repository) try: if not contents: raise HacsException("No content to download") for content in contents: if repository.repository_manifest.content_in_root: if repository.repository_manifest.filename is not None: if content.name != repository.repository_manifest.filename: continue repository.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file(repository.hass, content.download_url) if filecontent is None: validate.errors.append(f"[{content.name}] was not downloaded.") continue # Save the content of the file. if repository.content.single or content.path is None: local_directory = repository.content.path.local else: _content_path = content.path if not repository.repository_manifest.content_in_root: _content_path = _content_path.replace( f"{repository.content.path.remote}", "") local_directory = f"{repository.content.path.local}/{_content_path}" local_directory = local_directory.split("/") del local_directory[-1] local_directory = "/".join(local_directory) # Check local directory pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) local_file_path = f"{local_directory}/{content.name}" result = await async_save_file(local_file_path, filecontent) if result: repository.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except Exception as exception: # pylint: disable=broad-except validate.errors.append(f"Download was not complete [{exception}]") return validate
async def get_info_md_content(repository): """Get the content of info.md""" filename = info_file(repository) if not filename: return "" try: info = await repository.repository_object.get_contents(filename, repository.ref) if info is None: return "" info = info.content.replace("<svg", "<disabled").replace("</svg", "</disabled") return render_template(info, repository) except (AIOGitHubException, Exception): # pylint: disable=broad-except if repository.hacs.action: raise HacsException("No info file found") return ""
async def download_content(repository): """Download the content of a directory.""" contents = gather_files_to_download(repository) repository.logger.debug(repository.data.filename) if not contents: raise HacsException("No content to download") for content in contents: if repository.data.content_in_root and repository.data.filename: if content.name != repository.data.filename: continue repository.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file(content.download_url) if filecontent is None: repository.validate.errors.append( f"[{content.name}] was not downloaded.") continue # Save the content of the file. if repository.content.single or content.path is None: local_directory = repository.content.path.local else: _content_path = content.path if not repository.data.content_in_root: _content_path = _content_path.replace( f"{repository.content.path.remote}", "") local_directory = f"{repository.content.path.local}/{_content_path}" local_directory = local_directory.split("/") del local_directory[-1] local_directory = "/".join(local_directory) # Check local directory pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) local_file_path = (f"{local_directory}/{content.name}").replace( "//", "/") result = await async_save_file(local_file_path, filecontent) if result: repository.logger.info(f"download of {content.name} complete") continue repository.validate.errors.append( f"[{content.name}] was not downloaded.")
async def validate_repository(self): """Validate.""" # Run common validation steps. await self.common_validate() # Custom step 1: Validate content. find_file_name(self) if self.content.path.remote is None: raise HacsException( f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" ) if self.content.path.remote == "release": self.content.single = True # 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 download_content(repository, validate, local_directory): """Download the content of a directory.""" contents = [] try: if repository.releases.releases and repository.information.category in [ "plugin", "theme", ]: for release in repository.releases.objects: if repository.status.selected_tag == release.tag_name: for asset in release.assets: contents.append(asset) if not contents: if repository.content.single: for repository_object in repository.content.objects: contents.append(repository_object) else: tree = await repository.repository_object.get_tree( repository.ref) for path in tree: if path.is_directory: continue if repository.content.path.remote in path.full_path: path.name = path.filename contents.append(path) if not contents: raise HacsException("No content to download") for content in contents: repository.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file(repository.hass, content.download_url) if filecontent is None: validate.errors.append(f"[{content.name}] was not downloaded.") continue # Save the content of the file. if repository.content.single or content.path is None: local_directory = repository.content.path.local else: _content_path = content.path if not repository.repository_manifest.content_in_root: _content_path = _content_path.replace( f"{repository.content.path.remote}", "") local_directory = f"{repository.content.path.local}/{_content_path}" local_directory = local_directory.split("/") del local_directory[-1] local_directory = "/".join(local_directory) # Check local directory pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) local_file_path = f"{local_directory}/{content.name}" result = await async_save_file(local_file_path, filecontent) if result: repository.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except Exception as exception: # pylint: disable=broad-except validate.errors.append(f"Download was not complete [{exception}]") return validate
async def install_repository(repository): """Common installation steps of the repository.""" persistent_directory = None await repository.update_repository() if not repository.can_install: raise HacsException( "The version of Home Assistant is not compatible with this version" ) version = version_to_install(repository) if version == repository.information.default_branch: repository.ref = version else: repository.ref = f"tags/{version}" if repository.repository_manifest: if repository.repository_manifest.persistent_directory: if os.path.exists( f"{repository.content.path.local}/{repository.repository_manifest.persistent_directory}" ): persistent_directory = Backup( f"{repository.content.path.local}/{repository.repository_manifest.persistent_directory}", tempfile.gettempdir() + "/hacs_persistent_directory/", ) persistent_directory.create() if repository.status.installed and not repository.content.single: backup = Backup(repository.content.path.local) backup.create() if (repository.repository_manifest.zip_release and version != repository.information.default_branch): validate = await repository.download_zip(repository.validate) else: validate = await repository.download_content( repository.validate, repository.content.path.remote, repository.content.path.local, repository.ref, ) if validate.errors: for error in validate.errors: repository.logger.error(error) if repository.status.installed and not repository.content.single: backup.restore() if repository.status.installed and not repository.content.single: backup.cleanup() if persistent_directory is not None: persistent_directory.restore() persistent_directory.cleanup() if validate.success: if repository.information.full_name not in repository.common.installed: if repository.information.full_name == "hacs/integration": repository.common.installed.append( repository.information.full_name) repository.status.installed = True repository.versions.installed_commit = repository.versions.available_commit if version == repository.information.default_branch: repository.versions.installed = None else: repository.versions.installed = version await reload_after_install(repository) installation_complete(repository)
async def common_update_data(repository): """Common update data.""" hacs = get_hacs() try: repository_object = await get_repository(hacs.session, hacs.configuration.token, repository.data.full_name) repository.repository_object = repository_object repository.data.update_data(repository_object.attributes) except (AIOGitHubException, HacsException) as exception: if not hacs.system.status.startup: repository.logger.error(exception) repository.validate.errors.append("Repository does not exist.") raise HacsException(exception) # Make sure the repository is not archived. if repository.data.archived: repository.validate.errors.append("Repository is archived.") raise HacsException("Repository is archived.") # Make sure the repository is not in the blacklist. if is_removed(repository.data.full_name): repository.validate.errors.append("Repository is in the blacklist.") raise HacsException("Repository is in the blacklist.") # Get releases. try: releases = await get_releases( repository.repository_object, repository.status.show_beta, hacs.configuration.release_limit, ) if releases: repository.releases.releases = True repository.releases.objects = releases repository.releases.published_tags = [ x.tag_name for x in releases if not x.draft ] repository.versions.available = next(iter(releases)).tag_name for release in releases: if release.tag_name == repository.ref: assets = release.assets if assets: downloads = next( iter(assets)).attributes.get("download_count") repository.releases.downloads = downloads except (AIOGitHubException, HacsException): repository.releases.releases = False repository.ref = version_to_install(repository) repository.logger.debug( f"Running checks against {repository.ref.replace('tags/', '')}") try: repository.tree = await get_tree(repository.repository_object, repository.ref) if not repository.tree: raise HacsException("No files in tree") repository.treefiles = [] for treefile in repository.tree: repository.treefiles.append(treefile.full_path) except (AIOGitHubException, HacsException) as exception: if not hacs.system.status.startup: repository.logger.error(exception) raise HacsException(exception)
async def install_repository(repository): """Common installation steps of the repository.""" persistent_directory = None await repository.update_repository() if repository.content.path.local is None: raise HacsException("repository.content.path.local is None") repository.validate.errors = [] if not repository.can_install: raise HacsException( "The version of Home Assistant is not compatible with this version" ) version = version_to_install(repository) if version == repository.data.default_branch: repository.ref = version else: repository.ref = f"tags/{version}" if repository.data.installed and repository.data.category == "netdaemon": persistent_directory = BackupNetDaemon(repository) persistent_directory.create() elif repository.data.persistent_directory: if os.path.exists( f"{repository.content.path.local}/{repository.data.persistent_directory}" ): persistent_directory = Backup( f"{repository.content.path.local}/{repository.data.persistent_directory}", tempfile.gettempdir() + "/hacs_persistent_directory/", ) persistent_directory.create() if repository.data.installed and not repository.content.single: backup = Backup(repository.content.path.local) backup.create() if repository.data.zip_release and version != repository.data.default_branch: await repository.download_zip(repository) else: await download_content(repository) if repository.validate.errors: for error in repository.validate.errors: repository.logger.error(error) if repository.data.installed and not repository.content.single: backup.restore() if repository.data.installed and not repository.content.single: backup.cleanup() if persistent_directory is not None: persistent_directory.restore() persistent_directory.cleanup() if repository.validate.success: if repository.data.full_name not in repository.hacs.common.installed: if repository.data.full_name == "hacs/integration": repository.hacs.common.installed.append( repository.data.full_name) repository.data.installed = True repository.data.installed_commit = repository.data.last_commit if version == repository.data.default_branch: repository.data.installed_version = None else: repository.data.installed_version = version await reload_after_install(repository) installation_complete(repository)
async def common_validate(repository): """Common validation steps of the repository.""" hacs = get_hacs() repository.validate.errors = [] # Make sure the repository exist. repository.logger.debug("Checking repository.") try: repository_object = await get_repository( hacs.session, hacs.configuration.token, repository.information.full_name) repository.repository_object = repository_object repository.data = repository.data.create_from_dict( repository_object.attributes) except (AIOGitHubException, HacsException) as exception: if not hacs.system.status.startup: repository.logger.error(exception) repository.validate.errors.append("Repository does not exist.") raise HacsException(exception) # Make sure the repository is not archived. if repository.data.archived: repository.validate.errors.append("Repository is archived.") raise HacsException("Repository is archived.") # Make sure the repository is not in the blacklist. if repository.data.full_name in hacs.common.blacklist: repository.validate.errors.append("Repository is in the blacklist.") raise HacsException("Repository is in the blacklist.") # Get releases. try: releases = await get_releases( repository.repository_object, repository.status.show_beta, hacs.configuration.release_limit, ) if releases: repository.releases.releases = True repository.releases.published_tags = [x.tag_name for x in releases] repository.versions.available = next(iter(releases)).tag_name assets = next(iter(releases)).assets if assets: downloads = next(iter(assets)).attributes.get("download_count") repository.releases.downloads = downloads except (AIOGitHubException, HacsException): repository.releases.releases = False repository.ref = version_to_install(repository) repository.logger.debug( f"Running checks against {repository.ref.replace('tags/', '')}") try: repository.tree = await get_tree(repository.repository_object, repository.ref) if not repository.tree: raise HacsException("No files in tree") repository.treefiles = [] for treefile in repository.tree: repository.treefiles.append(treefile.full_path) except (AIOGitHubException, HacsException) as exception: if not hacs.system.status.startup: repository.logger.error(exception) raise HacsException(exception) # Step 6: Get the content of hacs.json await repository.get_repository_manifest_content() # Set repository name repository.information.name = repository.information.full_name.split( "/")[1]