"""Models for manifest validator.""" from __future__ import annotations import importlib import json import pathlib from types import ModuleType from typing import Any import attr @attr.s class Error: """Error validating an integration.""" plugin: str = attr.ib() error: str = attr.ib() fixable: bool = attr.ib(default=False) def __str__(self) -> str: """Represent error as string.""" return f"[{self.plugin.upper()}] {self.error}" @attr.s class Config: """Config for the run.""" specific_integrations: pathlib.Path | None = attr.ib() root: pathlib.Path = attr.ib() action: str = attr.ib() requirements: bool = attr.ib() errors: list[Error] = attr.ib(factory=list) cache: dict[str, Any] = attr.ib(factory=dict) plugins: set[str] = attr.ib(factory=set) def add_error(self, *args: Any, **kwargs: Any) -> None: """Add an error.""" self.errors.append(Error(*args, **kwargs)) @attr.s class Brand: """Represent a brand in our validator.""" @classmethod def load_dir(cls, path: pathlib.Path, config: Config) -> dict[str, Brand]: """Load all brands in a directory.""" assert path.is_dir() brands: dict[str, Brand] = {} for fil in path.iterdir(): brand = cls(fil) brand.load_brand(config) brands[brand.domain] = brand return brands path: pathlib.Path = attr.ib() brand: dict[str, Any] | None = attr.ib(default=None) @property def domain(self) -> str: """Integration domain.""" return self.path.stem @property def name(self) -> str | None: """Return name of the integration.""" assert self.brand is not None, "brand has not been loaded" return self.brand.get("name") @property def integrations(self) -> list[str]: """Return the sub integrations of this brand.""" assert self.brand is not None, "brand has not been loaded" return self.brand.get("integrations", []) @property def iot_standards(self) -> list[str]: """Return list of supported IoT standards.""" assert self.brand is not None, "brand has not been loaded" return self.brand.get("iot_standards", []) def load_brand(self, config: Config) -> None: """Load brand file.""" if not self.path.is_file(): config.add_error("model", f"Brand file {self.path} not found") return try: brand: dict[str, Any] = json.loads(self.path.read_text()) except ValueError as err: config.add_error( "model", f"Brand file {self.path.name} contains invalid JSON: {err}" ) return self.brand = brand @attr.s class Integration: """Represent an integration in our validator.""" @classmethod def load_dir(cls, path: pathlib.Path) -> dict[str, Integration]: """Load all integrations in a directory.""" assert path.is_dir() integrations: dict[str, Integration] = {} for fil in path.iterdir(): if fil.is_file() or fil.name == "__pycache__": continue init = fil / "__init__.py" manifest = fil / "manifest.json" if not init.exists() and not manifest.exists(): print( f"Warning: {init} and manifest.json missing, " "skipping directory. If this is your development " "environment, you can safely delete this folder." ) continue integration = cls(fil) integration.load_manifest() integrations[integration.domain] = integration return integrations path: pathlib.Path = attr.ib() manifest: dict[str, Any] | None = attr.ib(default=None) errors: list[Error] = attr.ib(factory=list) warnings: list[Error] = attr.ib(factory=list) translated_name: bool = attr.ib(default=False) @property def domain(self) -> str: """Integration domain.""" return self.path.name @property def core(self) -> bool: """Core integration.""" return self.path.as_posix().startswith("homeassistant/components") @property def disabled(self) -> str | None: """Return if integration is disabled.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("disabled") @property def name(self) -> str: """Return name of the integration.""" assert self.manifest is not None, "manifest has not been loaded" name: str = self.manifest["name"] return name @property def quality_scale(self) -> str | None: """Return quality scale of the integration.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("quality_scale") @property def config_flow(self) -> bool: """Return if the integration has a config flow.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("config_flow", False) @property def requirements(self) -> list[str]: """List of requirements.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("requirements", []) @property def dependencies(self) -> list[str]: """List of dependencies.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("dependencies", []) @property def supported_by(self) -> str: """Return the integration supported by this virtual integration.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("supported_by", {}) @property def integration_type(self) -> str: """Get integration_type.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("integration_type", "hub") @property def iot_class(self) -> str | None: """Return the integration IoT Class.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("iot_class") @property def iot_standards(self) -> list[str]: """Return the IoT standard supported by this virtual integration.""" assert self.manifest is not None, "manifest has not been loaded" return self.manifest.get("iot_standards", []) def add_error(self, *args: Any, **kwargs: Any) -> None: """Add an error.""" self.errors.append(Error(*args, **kwargs)) def add_warning(self, *args: Any, **kwargs: Any) -> None: """Add a warning.""" self.warnings.append(Error(*args, **kwargs)) def load_manifest(self) -> None: """Load manifest.""" manifest_path = self.path / "manifest.json" if not manifest_path.is_file(): self.add_error("model", f"Manifest file {manifest_path} not found") return try: manifest: dict[str, Any] = json.loads(manifest_path.read_text()) except ValueError as err: self.add_error("model", f"Manifest contains invalid JSON: {err}") return self.manifest = manifest def import_pkg(self, platform: str | None = None) -> ModuleType: """Import the Python file.""" pkg = f"homeassistant.components.{self.domain}" if platform is not None: pkg += f".{platform}" return importlib.import_module(pkg)