From c9bbc5e9efbfdf53c684959d5b37b1ee5b3b01c5 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 25 Jul 2020 14:07:48 +0100 Subject: [PATCH 01/43] Basic setup of integration --- CODEOWNERS | 1 + homeassistant/components/github/__init__.py | 44 ++- .../components/github/config_flow.py | 77 ++++++ homeassistant/components/github/const.py | 5 + homeassistant/components/github/manifest.json | 15 +- homeassistant/components/github/sensor.py | 258 +----------------- homeassistant/components/github/strings.json | 20 ++ .../components/github/translations/en.json | 20 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/github/__init__.py | 1 + tests/components/github/test_config_flow.py | 90 ++++++ 13 files changed, 288 insertions(+), 249 deletions(-) create mode 100644 homeassistant/components/github/config_flow.py create mode 100644 homeassistant/components/github/const.py create mode 100644 homeassistant/components/github/strings.json create mode 100644 homeassistant/components/github/translations/en.json create mode 100644 tests/components/github/__init__.py create mode 100644 tests/components/github/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index e3a7c5e73279c..09ec2edf7e8da 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,7 @@ homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/gios/* @bieniu +homeassistant/components/github/* @timmo001 homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/goalzero/* @tkdrob diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 6dd5d7f16ff68..637e3a9dc7863 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1 +1,43 @@ -"""The github component.""" +"""The GitHub integration.""" +import asyncio + +# from github import Github +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up GitHub from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py new file mode 100644 index 0000000000000..e3dd4d529e9de --- /dev/null +++ b/homeassistant/components/github/config_flow.py @@ -0,0 +1,77 @@ +"""Config flow for GitHub integration.""" +import logging + +from github import Github, GithubException, Repository +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_URL + +from .const import CONF_REPOSITORY, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_ACCESS_TOKEN): str, + vol.Required(CONF_REPOSITORY): str, + vol.Optional(CONF_URL): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + url = data[CONF_URL] + + try: + if url: + github = Github(data[CONF_ACCESS_TOKEN], base_url=url) + else: + github = Github(data[CONF_ACCESS_TOKEN]) + except GithubException.BadCredentialsException: + raise InvalidAuth + except GithubException.GithubException: + raise CannotConnect + + repository: Repository = github.get_repo(data[CONF_REPOSITORY]) + + return {"title": repository.name} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for GitHub.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py new file mode 100644 index 0000000000000..06ac5ba68e3ab --- /dev/null +++ b/homeassistant/components/github/const.py @@ -0,0 +1,5 @@ +"""Constants for the GitHub integration.""" + +DOMAIN = "github" + +CONF_REPOSITORY = "repository" diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 1a9cd620b0e8f..f040508230b66 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -1,7 +1,16 @@ { "domain": "github", "name": "GitHub", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/github", - "requirements": ["PyGithub==1.43.8"], - "codeowners": [] -} + "requirements": [ + "PyGithub==1.51.0" + ], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@timmo001" + ] +} \ No newline at end of file diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 312e726b91d39..315b5b21a7f7e 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,112 +1,24 @@ -"""Support for GitHub.""" -from datetime import timedelta -import logging - -import github -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_NAME, - CONF_ACCESS_TOKEN, - CONF_NAME, - CONF_PATH, - CONF_URL, -) -import homeassistant.helpers.config_validation as cv +"""Sensor platform for GitHub integration.""" +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) - -CONF_REPOS = "repositories" - -ATTR_LATEST_COMMIT_MESSAGE = "latest_commit_message" -ATTR_LATEST_COMMIT_SHA = "latest_commit_sha" -ATTR_LATEST_RELEASE_TAG = "latest_release_tag" -ATTR_LATEST_RELEASE_URL = "latest_release_url" -ATTR_LATEST_OPEN_ISSUE_URL = "latest_open_issue_url" -ATTR_OPEN_ISSUES = "open_issues" -ATTR_LATEST_OPEN_PULL_REQUEST_URL = "latest_open_pull_request_url" -ATTR_OPEN_PULL_REQUESTS = "open_pull_requests" -ATTR_PATH = "path" -ATTR_STARGAZERS = "stargazers" -ATTR_FORKS = "forks" -ATTR_CLONES = "clones" -ATTR_CLONES_UNIQUE = "clones_unique" -ATTR_VIEWS = "views" -ATTR_VIEWS_UNIQUE = "views_unique" - -DEFAULT_NAME = "GitHub" - -SCAN_INTERVAL = timedelta(seconds=300) - -REPO_SCHEMA = vol.Schema( - {vol.Required(CONF_PATH): cv.string, vol.Optional(CONF_NAME): cv.string} -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_URL): cv.url, - vol.Required(CONF_REPOS): vol.All(cv.ensure_list, [REPO_SCHEMA]), - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the GitHub sensor platform.""" - sensors = [] - for repository in config[CONF_REPOS]: - data = GitHubData( - repository=repository, - access_token=config.get(CONF_ACCESS_TOKEN), - server_url=config.get(CONF_URL), - ) - if data.setup_error is True: - _LOGGER.error( - "Error setting up GitHub platform. %s", - "Check previous errors for details", - ) - else: - sensors.append(GitHubSensor(data)) - add_entities(sensors, True) + """Set up the sensor platform.""" + add_entities([ExampleSensor()]) -class GitHubSensor(Entity): - """Representation of a GitHub sensor.""" +class ExampleSensor(Entity): + """Representation of a Sensor.""" - def __init__(self, github_data): - """Initialize the GitHub sensor.""" - self._unique_id = github_data.repository_path - self._name = None + def __init__(self): + """Initialize the sensor.""" self._state = None - self._available = False - self._repository_path = None - self._latest_commit_message = None - self._latest_commit_sha = None - self._latest_release_tag = None - self._latest_release_url = None - self._open_issue_count = None - self._latest_open_issue_url = None - self._pull_request_count = None - self._latest_open_pr_url = None - self._stargazers = None - self._forks = None - self._clones = None - self._clones_unique = None - self._views = None - self._views_unique = None - self._github_data = github_data @property def name(self): """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return unique ID for the sensor.""" - return self._unique_id + return "Example Temperature" @property def state(self): @@ -114,152 +26,10 @@ def state(self): return self._state @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = { - ATTR_PATH: self._repository_path, - ATTR_NAME: self._name, - ATTR_LATEST_COMMIT_MESSAGE: self._latest_commit_message, - ATTR_LATEST_COMMIT_SHA: self._latest_commit_sha, - ATTR_LATEST_RELEASE_URL: self._latest_release_url, - ATTR_LATEST_OPEN_ISSUE_URL: self._latest_open_issue_url, - ATTR_OPEN_ISSUES: self._open_issue_count, - ATTR_LATEST_OPEN_PULL_REQUEST_URL: self._latest_open_pr_url, - ATTR_OPEN_PULL_REQUESTS: self._pull_request_count, - ATTR_STARGAZERS: self._stargazers, - ATTR_FORKS: self._forks, - } - if self._latest_release_tag is not None: - attrs[ATTR_LATEST_RELEASE_TAG] = self._latest_release_tag - if self._clones is not None: - attrs[ATTR_CLONES] = self._clones - if self._clones_unique is not None: - attrs[ATTR_CLONES_UNIQUE] = self._clones_unique - if self._views is not None: - attrs[ATTR_VIEWS] = self._views - if self._views_unique is not None: - attrs[ATTR_VIEWS_UNIQUE] = self._views_unique - return attrs - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:github" - - def update(self): - """Collect updated data from GitHub API.""" - self._github_data.update() - - self._name = self._github_data.name - self._repository_path = self._github_data.repository_path - self._available = self._github_data.available - self._latest_commit_message = self._github_data.latest_commit_message - self._latest_commit_sha = self._github_data.latest_commit_sha - if self._github_data.latest_release_url is not None: - self._latest_release_tag = self._github_data.latest_release_url.split( - "tag/" - )[1] - else: - self._latest_release_tag = None - self._latest_release_url = self._github_data.latest_release_url - self._state = self._github_data.latest_commit_sha[0:7] - self._open_issue_count = self._github_data.open_issue_count - self._latest_open_issue_url = self._github_data.latest_open_issue_url - self._pull_request_count = self._github_data.pull_request_count - self._latest_open_pr_url = self._github_data.latest_open_pr_url - self._stargazers = self._github_data.stargazers - self._forks = self._github_data.forks - self._clones = self._github_data.clones - self._clones_unique = self._github_data.clones_unique - self._views = self._github_data.views - self._views_unique = self._github_data.views_unique - - -class GitHubData: - """GitHub Data object.""" - - def __init__(self, repository, access_token=None, server_url=None): - """Set up GitHub.""" - self._github = github - - self.setup_error = False - - try: - if server_url is not None: - server_url += "/api/v3" - self._github_obj = github.Github(access_token, base_url=server_url) - else: - self._github_obj = github.Github(access_token) - - self.repository_path = repository[CONF_PATH] - - repo = self._github_obj.get_repo(self.repository_path) - except self._github.GithubException as err: - _LOGGER.error("GitHub error for %s: %s", self.repository_path, err) - self.setup_error = True - return - - self.name = repository.get(CONF_NAME, repo.name) - self.available = False - self.latest_commit_message = None - self.latest_commit_sha = None - self.latest_release_url = None - self.open_issue_count = None - self.latest_open_issue_url = None - self.pull_request_count = None - self.latest_open_pr_url = None - self.stargazers = None - self.forks = None - self.clones = None - self.clones_unique = None - self.views = None - self.views_unique = None + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS def update(self): - """Update GitHub Sensor.""" - try: - repo = self._github_obj.get_repo(self.repository_path) - - self.stargazers = repo.stargazers_count - self.forks = repo.forks_count - - open_issues = repo.get_issues(state="open", sort="created") - if open_issues is not None: - self.open_issue_count = open_issues.totalCount - if open_issues.totalCount > 0: - self.latest_open_issue_url = open_issues[0].html_url - - open_pull_requests = repo.get_pulls(state="open", sort="created") - if open_pull_requests is not None: - self.pull_request_count = open_pull_requests.totalCount - if open_pull_requests.totalCount > 0: - self.latest_open_pr_url = open_pull_requests[0].html_url - - latest_commit = repo.get_commits()[0] - self.latest_commit_sha = latest_commit.sha - self.latest_commit_message = latest_commit.commit.message - - releases = repo.get_releases() - if releases and releases.totalCount > 0: - self.latest_release_url = releases[0].html_url - - if repo.permissions.push: - clones = repo.get_clones_traffic() - if clones is not None: - self.clones = clones.get("count") - self.clones_unique = clones.get("uniques") - - views = repo.get_views_traffic() - if views is not None: - self.views = views.get("count") - self.views_unique = views.get("uniques") - - self.available = True - except self._github.GithubException as err: - _LOGGER.error("GitHub error for %s: %s", self.repository_path, err) - self.available = False + """Fetch new state data for the sensor.""" + self._state = 23 diff --git a/homeassistant/components/github/strings.json b/homeassistant/components/github/strings.json new file mode 100644 index 0000000000000..aad6adaa02a5d --- /dev/null +++ b/homeassistant/components/github/strings.json @@ -0,0 +1,20 @@ +{ + "title": "GitHub", + "config": { + "step": { + "user": { + "description": "Please enter your access token and repository. URL is only needed if you are using a private Github server", + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]", + "repository": "Repository", + "url": "URL" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/github/translations/en.json b/homeassistant/components/github/translations/en.json new file mode 100644 index 0000000000000..aad6adaa02a5d --- /dev/null +++ b/homeassistant/components/github/translations/en.json @@ -0,0 +1,20 @@ +{ + "title": "GitHub", + "config": { + "step": { + "user": { + "description": "Please enter your access token and repository. URL is only needed if you are using a private Github server", + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]", + "repository": "Repository", + "url": "URL" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6266660546fea..379317655cf64 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -67,6 +67,7 @@ "geonetnz_quakes", "geonetnz_volcano", "gios", + "github", "glances", "goalzero", "gogogate2", diff --git a/requirements_all.txt b/requirements_all.txt index 83225c396429d..d52d8ab767608 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ PyEssent==0.13 PyFlick==0.0.2 # homeassistant.components.github -PyGithub==1.43.8 +PyGithub==1.51.0 # homeassistant.components.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d17dffacbb4a4..197fd994bf91e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,6 +12,9 @@ Plugwise_Smile==1.5.1 # homeassistant.components.flick_electric PyFlick==0.0.2 +# homeassistant.components.github +PyGithub==1.51.0 + # homeassistant.components.mobile_app # homeassistant.components.owntracks PyNaCl==1.3.0 diff --git a/tests/components/github/__init__.py b/tests/components/github/__init__.py new file mode 100644 index 0000000000000..55c9fb8699456 --- /dev/null +++ b/tests/components/github/__init__.py @@ -0,0 +1 @@ +"""Tests for the GitHub integration.""" diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py new file mode 100644 index 0000000000000..020ea79ce7cc6 --- /dev/null +++ b/tests/components/github/test_config_flow.py @@ -0,0 +1,90 @@ +"""Test the GitHub config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.github.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.github.const import DOMAIN + +from tests.async_mock import patch + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.github.config_flow.PlaceholderHub.authenticate", + return_value=True, + ), patch( + "homeassistant.components.github.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.github.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Name of the device" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.github.config_flow.PlaceholderHub.authenticate", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.github.config_flow.PlaceholderHub.authenticate", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From 133f9dfcd28797252982c6851dc029aafb0ae640 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 25 Jul 2020 18:50:30 +0100 Subject: [PATCH 02/43] Working sensor --- homeassistant/components/github/__init__.py | 29 +++- .../components/github/config_flow.py | 39 +++-- homeassistant/components/github/manifest.json | 2 +- homeassistant/components/github/sensor.py | 159 ++++++++++++++++-- homeassistant/components/github/strings.json | 8 +- .../components/github/translations/en.json | 8 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- 8 files changed, 205 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 637e3a9dc7863..d529b827539a1 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,23 +1,36 @@ """The GitHub integration.""" import asyncio -# from github import Github -import voluptuous as vol +from aiogithubapi import ( + AIOGitHubAPIAuthenticationException, + AIOGitHubAPIException, + GitHub, +) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import Config, HomeAssistant from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - PLATFORMS = ["sensor"] +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up GitHub integration.""" + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up GitHub from a config entry.""" - # TODO Store an API object for your platforms to access - # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + try: + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = GitHub( + entry.data[CONF_ACCESS_TOKEN] + ) + except AIOGitHubAPIAuthenticationException as err: + raise ConfigEntryNotReady from err + except AIOGitHubAPIException as err: + raise ConfigEntryNotReady from err for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index e3dd4d529e9de..5d362fbafd462 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -1,22 +1,23 @@ """Config flow for GitHub integration.""" import logging -from github import Github, GithubException, Repository +from aiogithubapi import ( + AIOGitHubAPIAuthenticationException, + AIOGitHubAPIException, + GitHub, +) +from aiogithubapi.objects.repository import AIOGitHubAPIRepository import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_URL +from homeassistant.const import CONF_ACCESS_TOKEN from .const import CONF_REPOSITORY, DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_ACCESS_TOKEN): str, - vol.Required(CONF_REPOSITORY): str, - vol.Optional(CONF_URL): str, - } + {vol.Required(CONF_ACCESS_TOKEN): str, vol.Required(CONF_REPOSITORY): str} ) @@ -25,21 +26,18 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - url = data[CONF_URL] - try: - if url: - github = Github(data[CONF_ACCESS_TOKEN], base_url=url) - else: - github = Github(data[CONF_ACCESS_TOKEN]) - except GithubException.BadCredentialsException: + github = GitHub(data[CONF_ACCESS_TOKEN]) + except AIOGitHubAPIAuthenticationException: raise InvalidAuth - except GithubException.GithubException: + except AIOGitHubAPIException: raise CannotConnect - repository: Repository = github.get_repo(data[CONF_REPOSITORY]) + repository: AIOGitHubAPIRepository = await github.get_repo(data[CONF_REPOSITORY]) + if repository is None: + raise CannotFindRepo - return {"title": repository.name} + return {"title": repository.full_name} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -54,10 +52,11 @@ async def async_step_user(self, user_input=None): if user_input is not None: try: info = await validate_input(self.hass, user_input) - return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: errors["base"] = "cannot_connect" + except CannotFindRepo: + errors["base"] = "cannot_find_repo" except InvalidAuth: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except @@ -73,5 +72,9 @@ class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" +class CannotFindRepo(exceptions.HomeAssistantError): + """Error to indicate repo cannot be found.""" + + class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index f040508230b66..b412133d62475 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "PyGithub==1.51.0" + "aiogithubapi==1.0.4" ], "ssdp": [], "zeroconf": [], diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 315b5b21a7f7e..28a15b30424fb 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,24 +1,85 @@ """Sensor platform for GitHub integration.""" -from homeassistant.const import TEMP_CELSIUS +from datetime import timedelta +import logging + +from aiogithubapi import GitHub +from aiogithubapi.objects.repository import ( + AIOGitHubAPIRepository, + AIOGitHubAPIRepositoryIssue, + AIOGitHubAPIRepositoryRelease, +) + +from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import Entity +from .const import CONF_REPOSITORY, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ATTR_DESCRIPTION = "description" +ATTR_FORKS = "forks" +ATTR_HOMEPAGE = "homepage" +ATTR_LATEST_COMMIT_MESSAGE = "latest_commit_message" +ATTR_LATEST_COMMIT_SHA = "latest_commit_sha" +ATTR_LATEST_OPEN_ISSUE_URL = "latest_open_issue_url" +ATTR_LATEST_OPEN_PULL_REQUEST_URL = "latest_open_pull_request_url" +ATTR_LATEST_RELEASE_TAG = "latest_release_tag" +ATTR_LATEST_RELEASE_URL = "latest_release_url" +ATTR_OPEN_ISSUES = "open_issues" +ATTR_OPEN_PULL_REQUESTS = "open_pull_requests" +ATTR_PATH = "path" +ATTR_STARGAZERS = "stargazers" +ATTR_TOPICS = "topics" +ATTR_WATCHERS = "watchers" + +SCAN_INTERVAL = timedelta(seconds=300) -def setup_platform(hass, config, add_entities, discovery_info=None): + +async def async_setup_entry(hass, entry, async_add_entities): """Set up the sensor platform.""" - add_entities([ExampleSensor()]) + github = hass.data[DOMAIN][entry.entry_id] + repository = entry.data[CONF_REPOSITORY] + + async_add_entities([RepositorySensor(github, repository)], True) -class ExampleSensor(Entity): +class RepositorySensor(Entity): """Representation of a Sensor.""" - def __init__(self): + def __init__(self, github: GitHub, repository: str): """Initialize the sensor.""" + self._github = github + self._repository = repository + self._unique_id = f"{repository}_sensor" + self._available = False + self._description = None + self._forks = None + self._homepage = None + self._latest_commit_message = None + self._latest_commit_sha = None + self._latest_open_issue_url = None + self._latest_open_pr_url = None + self._latest_release_tag = None + self._latest_release_url = None + self._name = None + self._open_issues = None + self._pull_requests = None + self._stargazers = None self._state = None + self._topics = None + self._views = None + self._views_unique = None + self._watchers = None + + @property + def unique_id(self): + """Return the unique_id of the sensor.""" + return self._unique_id @property def name(self): """Return the name of the sensor.""" - return "Example Temperature" + return self._name @property def state(self): @@ -26,10 +87,86 @@ def state(self): return self._state @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS + def available(self): + """Return True if entity is available.""" + return self._available - def update(self): + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_DESCRIPTION: self._description, + ATTR_FORKS: self._forks, + ATTR_LATEST_COMMIT_MESSAGE: self._latest_commit_message, + ATTR_LATEST_COMMIT_SHA: self._latest_commit_sha, + ATTR_LATEST_OPEN_ISSUE_URL: self._latest_open_issue_url, + ATTR_LATEST_OPEN_PULL_REQUEST_URL: self._latest_open_pr_url, + ATTR_LATEST_RELEASE_TAG: self._latest_release_tag, + ATTR_LATEST_RELEASE_URL: self._latest_release_url, + ATTR_NAME: self._name, + ATTR_OPEN_ISSUES: self._open_issues, + ATTR_OPEN_PULL_REQUESTS: self._pull_requests, + ATTR_PATH: self._repository, + ATTR_STARGAZERS: self._stargazers, + ATTR_TOPICS: self._topics, + ATTR_WATCHERS: self._watchers, + } + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:github" + + async def async_update(self): """Fetch new state data for the sensor.""" - self._state = 23 + repository: AIOGitHubAPIRepository = await self._github.get_repo( + self._repository + ) + if repository is None: + _LOGGER.error("Cannot find repository") + self._available = False + return + + last_commit = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" + ) + + releases: AIOGitHubAPIRepositoryRelease = await repository.get_releases() + + all_issues: [AIOGitHubAPIRepositoryIssue] = await repository.get_issues() + issues: [AIOGitHubAPIRepositoryIssue] = [] + pull_requests: [AIOGitHubAPIRepositoryIssue] = [] + for issue in all_issues: + if issue.state == "open": + if "pull" in issue.html_url: + pull_requests.append(issue) + else: + issues.append(issue) + + self._state = last_commit["commit"]["sha"][0:7] + + self._name = repository.attributes.get("name") + self._description = repository.description + self._topics = repository.topics + self._homepage = repository.attributes.get("homepage") + self._latest_commit_sha = last_commit["commit"]["sha"] + self._latest_release_tag = releases[0].tag_name if len(releases) > 1 else "" + self._latest_release_url = ( + f"https://github.com/{repository.full_name}/releases/{releases[0].tag_name}" + if len(releases) > 1 + else "" + ) + self._stargazers = repository.attributes.get("stargazers_count") + self._watchers = repository.attributes.get("watchers_count") + self._forks = repository.attributes.get("forks") + self._latest_commit_message = last_commit["commit"]["commit"][ + "message" + ].splitlines()[0] + self._open_issues = len(issues) + self._pull_requests = len(pull_requests) + self._latest_open_issue_url = issues[0].html_url if len(issues) > 1 else "" + self._latest_open_pr_url = ( + pull_requests[0].html_url if len(pull_requests) > 1 else "" + ) + + self._available = True diff --git a/homeassistant/components/github/strings.json b/homeassistant/components/github/strings.json index aad6adaa02a5d..1fc04fa5abb0e 100644 --- a/homeassistant/components/github/strings.json +++ b/homeassistant/components/github/strings.json @@ -3,16 +3,16 @@ "config": { "step": { "user": { - "description": "Please enter your access token and repository. URL is only needed if you are using a private Github server", + "description": "Please enter your access token and repository.", "data": { - "access_token": "[%key:common::config_flow::data::access_token%]", - "repository": "Repository", - "url": "URL" + "access_token": "Access Token", + "repository": "Repository" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_find_repo": "Cannot find repository", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" } diff --git a/homeassistant/components/github/translations/en.json b/homeassistant/components/github/translations/en.json index aad6adaa02a5d..1fc04fa5abb0e 100644 --- a/homeassistant/components/github/translations/en.json +++ b/homeassistant/components/github/translations/en.json @@ -3,16 +3,16 @@ "config": { "step": { "user": { - "description": "Please enter your access token and repository. URL is only needed if you are using a private Github server", + "description": "Please enter your access token and repository.", "data": { - "access_token": "[%key:common::config_flow::data::access_token%]", - "repository": "Repository", - "url": "URL" + "access_token": "Access Token", + "repository": "Repository" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_find_repo": "Cannot find repository", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" } diff --git a/requirements_all.txt b/requirements_all.txt index d52d8ab767608..2f22b62bd7603 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -34,9 +34,6 @@ PyEssent==0.13 # homeassistant.components.flick_electric PyFlick==0.0.2 -# homeassistant.components.github -PyGithub==1.51.0 - # homeassistant.components.mvglive PyMVGLive==1.1.4 @@ -171,6 +168,9 @@ aiofreepybox==0.0.8 # homeassistant.components.yi aioftp==0.12.0 +# homeassistant.components.github +aiogithubapi==1.0.4 + # homeassistant.components.guardian aioguardian==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 197fd994bf91e..a1bd4963a3620 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,9 +12,6 @@ Plugwise_Smile==1.5.1 # homeassistant.components.flick_electric PyFlick==0.0.2 -# homeassistant.components.github -PyGithub==1.51.0 - # homeassistant.components.mobile_app # homeassistant.components.owntracks PyNaCl==1.3.0 @@ -102,6 +99,9 @@ aioflo==0.4.1 # homeassistant.components.freebox aiofreepybox==0.0.8 +# homeassistant.components.github +aiogithubapi==1.0.4 + # homeassistant.components.guardian aioguardian==1.0.1 From 1ebd9cfa149425315af84301e180d0fc6108c405 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 25 Jul 2020 23:10:46 +0100 Subject: [PATCH 03/43] Add missing attrs --- homeassistant/components/github/sensor.py | 43 ++++++++++++++++------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 28a15b30424fb..39964869a366a 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -16,6 +16,8 @@ _LOGGER = logging.getLogger(__name__) +ATTR_CLONES = "clones" +ATTR_CLONES_UNIQUE = "clones_unique" ATTR_DESCRIPTION = "description" ATTR_FORKS = "forks" ATTR_HOMEPAGE = "homepage" @@ -30,8 +32,11 @@ ATTR_PATH = "path" ATTR_STARGAZERS = "stargazers" ATTR_TOPICS = "topics" +ATTR_VIEWS = "views" +ATTR_VIEWS_UNIQUE = "views_unique" ATTR_WATCHERS = "watchers" + SCAN_INTERVAL = timedelta(seconds=300) @@ -52,6 +57,8 @@ def __init__(self, github: GitHub, repository: str): self._repository = repository self._unique_id = f"{repository}_sensor" self._available = False + self._clones = None + self._clones_unique = None self._description = None self._forks = None self._homepage = None @@ -95,6 +102,8 @@ def available(self): def device_state_attributes(self): """Return the state attributes.""" return { + ATTR_CLONES_UNIQUE: self._clones_unique, + ATTR_CLONES: self._clones, ATTR_DESCRIPTION: self._description, ATTR_FORKS: self._forks, ATTR_LATEST_COMMIT_MESSAGE: self._latest_commit_message, @@ -109,6 +118,8 @@ def device_state_attributes(self): ATTR_PATH: self._repository, ATTR_STARGAZERS: self._stargazers, ATTR_TOPICS: self._topics, + ATTR_VIEWS_UNIQUE: self._views_unique, + ATTR_VIEWS: self._views, ATTR_WATCHERS: self._watchers, } @@ -131,6 +142,10 @@ async def async_update(self): endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" ) + clones = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/clones" + ) + releases: AIOGitHubAPIRepositoryRelease = await repository.get_releases() all_issues: [AIOGitHubAPIRepositoryIssue] = await repository.get_issues() @@ -145,28 +160,32 @@ async def async_update(self): self._state = last_commit["commit"]["sha"][0:7] - self._name = repository.attributes.get("name") + self._clones = clones["count"] + self._clones_unique = clones["uniques"] self._description = repository.description - self._topics = repository.topics + self._forks = repository.attributes.get("forks") self._homepage = repository.attributes.get("homepage") + self._latest_commit_message = last_commit["commit"]["commit"][ + "message" + ].splitlines()[0] self._latest_commit_sha = last_commit["commit"]["sha"] + self._latest_open_issue_url = issues[0].html_url if len(issues) > 1 else "" + self._latest_open_pr_url = ( + pull_requests[0].html_url if len(pull_requests) > 1 else "" + ) self._latest_release_tag = releases[0].tag_name if len(releases) > 1 else "" self._latest_release_url = ( f"https://github.com/{repository.full_name}/releases/{releases[0].tag_name}" if len(releases) > 1 else "" ) - self._stargazers = repository.attributes.get("stargazers_count") - self._watchers = repository.attributes.get("watchers_count") - self._forks = repository.attributes.get("forks") - self._latest_commit_message = last_commit["commit"]["commit"][ - "message" - ].splitlines()[0] + self._name = repository.attributes.get("name") self._open_issues = len(issues) self._pull_requests = len(pull_requests) - self._latest_open_issue_url = issues[0].html_url if len(issues) > 1 else "" - self._latest_open_pr_url = ( - pull_requests[0].html_url if len(pull_requests) > 1 else "" - ) + self._stargazers = repository.attributes.get("stargazers_count") + self._topics = repository.topics + self._views = "" + self._views_unique = "" + self._watchers = repository.attributes.get("watchers_count") self._available = True From 088e433b536575a5c839dd2e3d7114cd453b039f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 00:38:31 +0100 Subject: [PATCH 04/43] Reauth flow --- homeassistant/components/github/__init__.py | 23 ++++--- .../components/github/config_flow.py | 61 +++++++++++++++++-- homeassistant/components/github/strings.json | 12 ++++ .../components/github/translations/en.json | 12 ++++ 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index d529b827539a1..e43453c7d3fae 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,5 +1,6 @@ """The GitHub integration.""" import asyncio +import logging from aiogithubapi import ( AIOGitHubAPIAuthenticationException, @@ -7,11 +8,13 @@ GitHub, ) -from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import Config, HomeAssistant -from .const import DOMAIN +from .const import CONF_REPOSITORY, DOMAIN + +_LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] @@ -24,13 +27,17 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up GitHub from a config entry.""" try: - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = GitHub( - entry.data[CONF_ACCESS_TOKEN] + github = GitHub(entry.data[CONF_ACCESS_TOKEN]) + await github.get_repo(entry.data[CONF_REPOSITORY]) + except (AIOGitHubAPIAuthenticationException, AIOGitHubAPIException): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) ) - except AIOGitHubAPIAuthenticationException as err: - raise ConfigEntryNotReady from err - except AIOGitHubAPIException as err: - raise ConfigEntryNotReady from err + return False + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = github for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 5d362fbafd462..a7ea73f007121 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -20,6 +20,8 @@ {vol.Required(CONF_ACCESS_TOKEN): str, vol.Required(CONF_REPOSITORY): str} ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. @@ -28,15 +30,16 @@ async def validate_input(hass: core.HomeAssistant, data): """ try: github = GitHub(data[CONF_ACCESS_TOKEN]) + repository: AIOGitHubAPIRepository = await github.get_repo( + data[CONF_REPOSITORY] + ) + if repository is None: + raise CannotFindRepo except AIOGitHubAPIAuthenticationException: raise InvalidAuth except AIOGitHubAPIException: raise CannotConnect - repository: AIOGitHubAPIRepository = await github.get_repo(data[CONF_REPOSITORY]) - if repository is None: - raise CannotFindRepo - return {"title": repository.full_name} @@ -46,6 +49,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Initialize the config flow.""" + self.access_token = None + self.repository = None + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -67,6 +75,51 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + async def async_step_reauth(self, user_input): + """Handle configuration by re-auth.""" + errors = {} + + if user_input is None: + user_input = {} + + if user_input.get(CONF_ACCESS_TOKEN) is None and self.access_token is not None: + user_input[CONF_ACCESS_TOKEN] = self.access_token + else: + self.access_token = user_input[CONF_ACCESS_TOKEN] + + if user_input.get(CONF_REPOSITORY) is None and self.repository is not None: + user_input[CONF_REPOSITORY] = self.repository + else: + self.repository = user_input[CONF_REPOSITORY] + + if self.context is None: + self.context = {} + # pylint: disable=no-member + self.context["title_placeholders"] = { + "name": user_input[CONF_REPOSITORY], + } + try: + await validate_input(self.hass, user_input) + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, data=user_input, + ) + return self.async_abort(reason="reauth_successful") + except CannotConnect: + errors["base"] = "cannot_connect" + except CannotFindRepo: + errors["base"] = "cannot_find_repo" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors + ) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/github/strings.json b/homeassistant/components/github/strings.json index 1fc04fa5abb0e..4f68f8f03642b 100644 --- a/homeassistant/components/github/strings.json +++ b/homeassistant/components/github/strings.json @@ -1,7 +1,15 @@ { "title": "GitHub", "config": { + "flow_title": "GitHub: {repository}", "step": { + "reauth": { + "title": "GitHub: {repository}", + "description": "Please re-enter your access token.", + "data": { + "access_token": "Access Token" + } + }, "user": { "description": "Please enter your access token and repository.", "data": { @@ -15,6 +23,10 @@ "cannot_find_repo": "Cannot find repository", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully" } } } diff --git a/homeassistant/components/github/translations/en.json b/homeassistant/components/github/translations/en.json index 1fc04fa5abb0e..4f68f8f03642b 100644 --- a/homeassistant/components/github/translations/en.json +++ b/homeassistant/components/github/translations/en.json @@ -1,7 +1,15 @@ { "title": "GitHub", "config": { + "flow_title": "GitHub: {repository}", "step": { + "reauth": { + "title": "GitHub: {repository}", + "description": "Please re-enter your access token.", + "data": { + "access_token": "Access Token" + } + }, "user": { "description": "Please enter your access token and repository.", "data": { @@ -15,6 +23,10 @@ "cannot_find_repo": "Cannot find repository", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully" } } } From db9d08e4f92c3a9e41d19cf05c67d45c0766395c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 00:43:07 +0100 Subject: [PATCH 05/43] Add views --- homeassistant/components/github/sensor.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 39964869a366a..31284f0366af0 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -138,16 +138,25 @@ async def async_update(self): self._available = False return + # TODO: Disable by default using option flow last_commit = await repository.client.get( endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" ) + # TODO: Disable by default using option flow clones = await repository.client.get( endpoint=f"/repos/{repository.full_name}/traffic/clones" ) + # TODO: Disable by default using option flow + views = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/views" + ) + + # TODO: Disable by default using option flow releases: AIOGitHubAPIRepositoryRelease = await repository.get_releases() + # TODO: Disable by default using option flow all_issues: [AIOGitHubAPIRepositoryIssue] = await repository.get_issues() issues: [AIOGitHubAPIRepositoryIssue] = [] pull_requests: [AIOGitHubAPIRepositoryIssue] = [] @@ -184,8 +193,8 @@ async def async_update(self): self._pull_requests = len(pull_requests) self._stargazers = repository.attributes.get("stargazers_count") self._topics = repository.topics - self._views = "" - self._views_unique = "" + self._views = views["count"] + self._views_unique = views["uniques"] self._watchers = repository.attributes.get("watchers_count") self._available = True From ef591ea27c1188a9a458bdad63dcce1ff0961a72 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 00:44:34 +0100 Subject: [PATCH 06/43] Order --- homeassistant/components/github/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 31284f0366af0..d98cbf44004c9 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -102,8 +102,8 @@ def available(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_CLONES_UNIQUE: self._clones_unique, ATTR_CLONES: self._clones, + ATTR_CLONES_UNIQUE: self._clones_unique, ATTR_DESCRIPTION: self._description, ATTR_FORKS: self._forks, ATTR_LATEST_COMMIT_MESSAGE: self._latest_commit_message, @@ -118,8 +118,8 @@ def device_state_attributes(self): ATTR_PATH: self._repository, ATTR_STARGAZERS: self._stargazers, ATTR_TOPICS: self._topics, - ATTR_VIEWS_UNIQUE: self._views_unique, ATTR_VIEWS: self._views, + ATTR_VIEWS_UNIQUE: self._views_unique, ATTR_WATCHERS: self._watchers, } From e595596e2124fb2707d173aa3e07c2b508976820 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 00:46:30 +0100 Subject: [PATCH 07/43] Add homepage --- homeassistant/components/github/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index d98cbf44004c9..64a7903418107 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -106,6 +106,7 @@ def device_state_attributes(self): ATTR_CLONES_UNIQUE: self._clones_unique, ATTR_DESCRIPTION: self._description, ATTR_FORKS: self._forks, + ATTR_HOMEPAGE: self._homepage, ATTR_LATEST_COMMIT_MESSAGE: self._latest_commit_message, ATTR_LATEST_COMMIT_SHA: self._latest_commit_sha, ATTR_LATEST_OPEN_ISSUE_URL: self._latest_open_issue_url, From aab33773c43e1759b5e94fa72e9c5825c285c781 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 01:03:35 +0100 Subject: [PATCH 08/43] Super classes --- homeassistant/components/github/__init__.py | 50 +++++++++++++++++++++ homeassistant/components/github/sensor.py | 30 +++---------- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index e43453c7d3fae..0f61d7265530d 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,6 +1,7 @@ """The GitHub integration.""" import asyncio import logging +from typing import Any, Dict from aiogithubapi import ( AIOGitHubAPIAuthenticationException, @@ -11,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.entity import Entity from .const import CONF_REPOSITORY, DOMAIN @@ -61,3 +63,51 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class GitHubEntity(Entity): + """Defines a GitHub entity.""" + + def __init__( + self, github: GitHub, repository: str, unique_id: str, name: str, icon: str + ) -> None: + """Set up GitHub Entity.""" + self._github = github + self._repository = repository + self._unique_id = unique_id + self._name = name + self._icon = icon + self._available = True + + @property + def unique_id(self): + """Return the unique_id of the sensor.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + +class GitHubDeviceEntity(GitHubEntity): + """Defines a GitHub device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this GitHub instance.""" + return { + "identifiers": {(DOMAIN, self._repository)}, + "manufacturer": self._repository.split("/")[0], + "name": self._repository, + } diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 64a7903418107..658cc97dd9b6f 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -10,8 +10,8 @@ ) from homeassistant.const import ATTR_NAME -from homeassistant.helpers.entity import Entity +from . import GitHubDeviceEntity from .const import CONF_REPOSITORY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -48,15 +48,11 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities([RepositorySensor(github, repository)], True) -class RepositorySensor(Entity): +class RepositorySensor(GitHubDeviceEntity): """Representation of a Sensor.""" def __init__(self, github: GitHub, repository: str): """Initialize the sensor.""" - self._github = github - self._repository = repository - self._unique_id = f"{repository}_sensor" - self._available = False self._clones = None self._clones_unique = None self._description = None @@ -78,26 +74,15 @@ def __init__(self, github: GitHub, repository: str): self._views_unique = None self._watchers = None - @property - def unique_id(self): - """Return the unique_id of the sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name + super().__init__( + github, repository, f"{repository}_sensor", repository, "mdi:github" + ) @property def state(self): """Return the state of the sensor.""" return self._state - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def device_state_attributes(self): """Return the state attributes.""" @@ -124,11 +109,6 @@ def device_state_attributes(self): ATTR_WATCHERS: self._watchers, } - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:github" - async def async_update(self): """Fetch new state data for the sensor.""" repository: AIOGitHubAPIRepository = await self._github.get_repo( From 8df4193a77476bdd395e2b8f0cf330ec988bf49e Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 02:20:28 +0100 Subject: [PATCH 09/43] Data update coordinator --- homeassistant/components/github/__init__.py | 155 ++++++++++++++++++-- homeassistant/components/github/const.py | 3 + homeassistant/components/github/sensor.py | 133 +++++++---------- 3 files changed, 200 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 0f61d7265530d..a13fb7b2db155 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,26 +1,98 @@ """The GitHub integration.""" import asyncio +from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any, Dict, List from aiogithubapi import ( AIOGitHubAPIAuthenticationException, AIOGitHubAPIException, GitHub, ) +from aiogithubapi.objects.repository import ( + AIOGitHubAPIRepository, + AIOGitHubAPIRepositoryIssue, + AIOGitHubAPIRepositoryRelease, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import Config, HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_REPOSITORY, DOMAIN +from .const import CONF_REPOSITORY, DATA_COORDINATOR, DATA_REPOSITORY, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] +class GitHubClones: + """Represents a GitHub clones object.""" + + def __init__(self, count: int, count_uniques: int) -> None: + """Initialize a GitHub clones object.""" + self.count = count + self.count_uniques = count_uniques + + +class GitHubLastCommit: + """Represents a GitHub last commit object.""" + + def __init__(self, sha: int, message: str) -> None: + """Initialize a GitHub last commit object.""" + self.sha = sha + self.sha_short = sha[0:7] + self.message = message.splitlines()[0] + + +class GitHubViews: + """Represents a GitHub views object.""" + + def __init__(self, count: int, count_uniques: int) -> None: + """Initialize a GitHub views object.""" + self.count = count + self.count_uniques = count_uniques + + +class GitHubData: + """Represents a GitHub data object.""" + + def __init__( + self, + repository: AIOGitHubAPIRepository, + last_commit: GitHubLastCommit, + clones: GitHubClones = None, + issues: List[AIOGitHubAPIRepositoryIssue] = None, + releases: List[AIOGitHubAPIRepositoryRelease] = None, + views: GitHubViews = None, + ) -> None: + """Initialize the GitHub data object.""" + self.repository = repository + self.last_commit = last_commit + self.clones = clones + self.issues = issues + self.releases = releases + self.views = views + + if issues is not None: + open_issues: List[AIOGitHubAPIRepositoryIssue] = [] + open_pull_requests: List[AIOGitHubAPIRepositoryIssue] = [] + for issue in issues: + if issue.state == "open": + if "pull" in issue.html_url: + open_pull_requests.append(issue) + else: + open_issues.append(issue) + + self.open_issues = open_issues + self.open_pull_requests = open_pull_requests + else: + self.open_issues = None + self.open_pull_requests = None + + async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up GitHub integration.""" return True @@ -30,7 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up GitHub from a config entry.""" try: github = GitHub(entry.data[CONF_ACCESS_TOKEN]) - await github.get_repo(entry.data[CONF_REPOSITORY]) + repository = await github.get_repo(entry.data[CONF_REPOSITORY]) except (AIOGitHubAPIAuthenticationException, AIOGitHubAPIException): hass.async_create_task( hass.config_entries.flow.async_init( @@ -39,7 +111,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) return False - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = github + # TODO: Disable extra REST calls by default using option flow + async def async_update_data() -> GitHubData: + """Fetch data from GitHub.""" + repository: AIOGitHubAPIRepository = await github.get_repo( + entry.data[CONF_REPOSITORY] + ) + last_commit = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" + ) + clones = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/clones" + ) + issues: [AIOGitHubAPIRepositoryIssue] = await repository.get_issues() + releases: [AIOGitHubAPIRepositoryRelease] = await repository.get_releases() + views = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/views" + ) + return GitHubData( + repository, + GitHubLastCommit( + last_commit["commit"]["sha"], last_commit["commit"]["commit"]["message"] + ), + GitHubClones(clones["count"], clones["uniques"]), + issues, + releases, + GitHubViews(views["count"], views["uniques"]), + ) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name=DOMAIN, + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=300), + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_REPOSITORY: repository, + } + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() for component in PLATFORMS: hass.async_create_task( @@ -69,11 +185,10 @@ class GitHubEntity(Entity): """Defines a GitHub entity.""" def __init__( - self, github: GitHub, repository: str, unique_id: str, name: str, icon: str + self, coordinator: DataUpdateCoordinator, unique_id: str, name: str, icon: str ) -> None: """Set up GitHub Entity.""" - self._github = github - self._repository = repository + self._coordinator = coordinator self._unique_id = unique_id self._name = name self._icon = icon @@ -97,7 +212,23 @@ def icon(self) -> str: @property def available(self) -> bool: """Return True if entity is available.""" - return self._available + return self._coordinator.last_update_success and self._available + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + async def async_update(self) -> None: + """Update GitHub entity.""" + if await self._github_update(): + self._available = True + else: + self._available = False + + async def _github_update(self) -> bool: + """Update GitHub entity.""" + raise NotImplementedError() class GitHubDeviceEntity(GitHubEntity): @@ -106,8 +237,10 @@ class GitHubDeviceEntity(GitHubEntity): @property def device_info(self) -> Dict[str, Any]: """Return device information about this GitHub instance.""" + data: GitHubData = self._coordinator.data + return { - "identifiers": {(DOMAIN, self._repository)}, - "manufacturer": self._repository.split("/")[0], - "name": self._repository, + "identifiers": {(DOMAIN, data.repository.full_name)}, + "manufacturer": data.repository.attributes.get("owner").get("login"), + "name": data.repository.full_name, } diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index 06ac5ba68e3ab..44f0c40513b82 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -1,5 +1,8 @@ """Constants for the GitHub integration.""" +DATA_COORDINATOR = "coordinator" +DATA_REPOSITORY = "repository" + DOMAIN = "github" CONF_REPOSITORY = "repository" diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 658cc97dd9b6f..8fa957779a570 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -2,17 +2,13 @@ from datetime import timedelta import logging -from aiogithubapi import GitHub -from aiogithubapi.objects.repository import ( - AIOGitHubAPIRepository, - AIOGitHubAPIRepositoryIssue, - AIOGitHubAPIRepositoryRelease, -) +from aiogithubapi.objects.repository import AIOGitHubAPIRepository from homeassistant.const import ATTR_NAME +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import GitHubDeviceEntity -from .const import CONF_REPOSITORY, DOMAIN +from . import GitHubData, GitHubDeviceEntity +from .const import DATA_COORDINATOR, DATA_REPOSITORY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -36,22 +32,28 @@ ATTR_VIEWS_UNIQUE = "views_unique" ATTR_WATCHERS = "watchers" - SCAN_INTERVAL = timedelta(seconds=300) +PARALLEL_UPDATES = 4 async def async_setup_entry(hass, entry, async_add_entities): """Set up the sensor platform.""" - github = hass.data[DOMAIN][entry.entry_id] - repository = entry.data[CONF_REPOSITORY] + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + repository: AIOGitHubAPIRepository = hass.data[DOMAIN][entry.entry_id][ + DATA_REPOSITORY + ] - async_add_entities([RepositorySensor(github, repository)], True) + async_add_entities([RepositorySensor(coordinator, repository)], True) class RepositorySensor(GitHubDeviceEntity): """Representation of a Sensor.""" - def __init__(self, github: GitHub, repository: str): + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: """Initialize the sensor.""" self._clones = None self._clones_unique = None @@ -75,16 +77,19 @@ def __init__(self, github: GitHub, repository: str): self._watchers = None super().__init__( - github, repository, f"{repository}_sensor", repository, "mdi:github" + coordinator, + f"{repository.full_name}_sensor", + repository.full_name, + "mdi:github", ) @property - def state(self): + def state(self) -> str: """Return the state of the sensor.""" return self._state @property - def device_state_attributes(self): + def device_state_attributes(self) -> dict: """Return the state attributes.""" return { ATTR_CLONES: self._clones, @@ -101,7 +106,6 @@ def device_state_attributes(self): ATTR_NAME: self._name, ATTR_OPEN_ISSUES: self._open_issues, ATTR_OPEN_PULL_REQUESTS: self._pull_requests, - ATTR_PATH: self._repository, ATTR_STARGAZERS: self._stargazers, ATTR_TOPICS: self._topics, ATTR_VIEWS: self._views, @@ -109,73 +113,42 @@ def device_state_attributes(self): ATTR_WATCHERS: self._watchers, } - async def async_update(self): + async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" - repository: AIOGitHubAPIRepository = await self._github.get_repo( - self._repository - ) - if repository is None: - _LOGGER.error("Cannot find repository") - self._available = False - return - - # TODO: Disable by default using option flow - last_commit = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" + data: GitHubData = self._coordinator.data + + self._state = data.last_commit.sha_short + + self._clones = data.clones.count + self._clones_unique = data.clones.count_uniques + self._description = data.repository.description + self._forks = data.repository.attributes.get("forks") + self._homepage = data.repository.attributes.get("homepage") + self._latest_commit_message = data.last_commit.message + self._latest_commit_sha = data.last_commit.sha + self._latest_open_issue_url = ( + data.open_issues[0].html_url if len(data.open_issues) > 1 else "" ) - - # TODO: Disable by default using option flow - clones = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/clones" - ) - - # TODO: Disable by default using option flow - views = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/views" - ) - - # TODO: Disable by default using option flow - releases: AIOGitHubAPIRepositoryRelease = await repository.get_releases() - - # TODO: Disable by default using option flow - all_issues: [AIOGitHubAPIRepositoryIssue] = await repository.get_issues() - issues: [AIOGitHubAPIRepositoryIssue] = [] - pull_requests: [AIOGitHubAPIRepositoryIssue] = [] - for issue in all_issues: - if issue.state == "open": - if "pull" in issue.html_url: - pull_requests.append(issue) - else: - issues.append(issue) - - self._state = last_commit["commit"]["sha"][0:7] - - self._clones = clones["count"] - self._clones_unique = clones["uniques"] - self._description = repository.description - self._forks = repository.attributes.get("forks") - self._homepage = repository.attributes.get("homepage") - self._latest_commit_message = last_commit["commit"]["commit"][ - "message" - ].splitlines()[0] - self._latest_commit_sha = last_commit["commit"]["sha"] - self._latest_open_issue_url = issues[0].html_url if len(issues) > 1 else "" self._latest_open_pr_url = ( - pull_requests[0].html_url if len(pull_requests) > 1 else "" + data.open_pull_requests[0].html_url + if len(data.open_pull_requests) > 1 + else "" + ) + self._latest_release_tag = ( + data.releases[0].tag_name if len(data.releases) > 1 else "" ) - self._latest_release_tag = releases[0].tag_name if len(releases) > 1 else "" self._latest_release_url = ( - f"https://github.com/{repository.full_name}/releases/{releases[0].tag_name}" - if len(releases) > 1 + f"https://github.com/{data.repository.full_name}/releases/{data.releases[0].tag_name}" + if len(data.releases) > 1 else "" ) - self._name = repository.attributes.get("name") - self._open_issues = len(issues) - self._pull_requests = len(pull_requests) - self._stargazers = repository.attributes.get("stargazers_count") - self._topics = repository.topics - self._views = views["count"] - self._views_unique = views["uniques"] - self._watchers = repository.attributes.get("watchers_count") - - self._available = True + self._name = data.repository.attributes.get("name") + self._open_issues = len(data.open_issues) + self._pull_requests = len(data.open_pull_requests) + self._stargazers = data.repository.attributes.get("stargazers_count") + self._topics = data.repository.topics + self._views = data.views.count + self._views_unique = data.views.count_uniques + self._watchers = data.repository.attributes.get("watchers_count") + + return True From 52bcd1f8fa515209cd74f9a2bcde7660382fc376 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 02:34:20 +0100 Subject: [PATCH 10/43] Sensor entity --- homeassistant/components/github/sensor.py | 32 ++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 8fa957779a570..31ca918c770eb 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -48,8 +48,23 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities([RepositorySensor(coordinator, repository)], True) -class RepositorySensor(GitHubDeviceEntity): - """Representation of a Sensor.""" +class GitHubSensor(GitHubDeviceEntity): + """Representation of a GitHub sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + repository: AIOGitHubAPIRepository, + unique_id: str, + name: str, + icon: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, f"{repository.full_name}_{unique_id}", name, icon) + + +class RepositorySensor(GitHubSensor): + """Representation of a Repository Sensor.""" def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository @@ -66,9 +81,9 @@ def __init__( self._latest_open_pr_url = None self._latest_release_tag = None self._latest_release_url = None - self._name = None self._open_issues = None self._pull_requests = None + self._repo_name = None self._stargazers = None self._state = None self._topics = None @@ -76,11 +91,10 @@ def __init__( self._views_unique = None self._watchers = None + name = repository.attributes.get("name") + super().__init__( - coordinator, - f"{repository.full_name}_sensor", - repository.full_name, - "mdi:github", + coordinator, repository, "repository", f"{name} Repository", "mdi:github" ) @property @@ -103,7 +117,7 @@ def device_state_attributes(self) -> dict: ATTR_LATEST_OPEN_PULL_REQUEST_URL: self._latest_open_pr_url, ATTR_LATEST_RELEASE_TAG: self._latest_release_tag, ATTR_LATEST_RELEASE_URL: self._latest_release_url, - ATTR_NAME: self._name, + ATTR_NAME: self._repo_name, ATTR_OPEN_ISSUES: self._open_issues, ATTR_OPEN_PULL_REQUESTS: self._pull_requests, ATTR_STARGAZERS: self._stargazers, @@ -142,9 +156,9 @@ async def _github_update(self) -> bool: if len(data.releases) > 1 else "" ) - self._name = data.repository.attributes.get("name") self._open_issues = len(data.open_issues) self._pull_requests = len(data.open_pull_requests) + self._repo_name = data.repository.attributes.get("name") self._stargazers = data.repository.attributes.get("stargazers_count") self._topics = data.repository.topics self._views = data.views.count From 95ef5b4df6ee449d7c5a31b3fc91fe3694bbc28d Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 02:49:49 +0100 Subject: [PATCH 11/43] Move attributes set into update --- homeassistant/components/github/sensor.py | 136 ++++++++-------------- 1 file changed, 50 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 31ca918c770eb..dee901841b802 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass, entry, async_add_entities): DATA_REPOSITORY ] - async_add_entities([RepositorySensor(coordinator, repository)], True) + async_add_entities([LastCommitSensor(coordinator, repository)], True) class GitHubSensor(GitHubDeviceEntity): @@ -60,109 +60,73 @@ def __init__( icon: str, ) -> None: """Initialize the sensor.""" + self._state = None + self._attributes = None + super().__init__(coordinator, f"{repository.full_name}_{unique_id}", name, icon) + @property + def state(self) -> str: + """Return the state of the sensor.""" + return self._state -class RepositorySensor(GitHubSensor): + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + return self._attributes + + +class LastCommitSensor(GitHubSensor): """Representation of a Repository Sensor.""" def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - self._clones = None - self._clones_unique = None - self._description = None - self._forks = None - self._homepage = None - self._latest_commit_message = None - self._latest_commit_sha = None - self._latest_open_issue_url = None - self._latest_open_pr_url = None - self._latest_release_tag = None - self._latest_release_url = None - self._open_issues = None - self._pull_requests = None - self._repo_name = None - self._stargazers = None - self._state = None - self._topics = None - self._views = None - self._views_unique = None - self._watchers = None - name = repository.attributes.get("name") - super().__init__( - coordinator, repository, "repository", f"{name} Repository", "mdi:github" + coordinator, repository, "last_commit", f"{name} Last Commit", "mdi:github" ) - @property - def state(self) -> str: - """Return the state of the sensor.""" - return self._state - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return { - ATTR_CLONES: self._clones, - ATTR_CLONES_UNIQUE: self._clones_unique, - ATTR_DESCRIPTION: self._description, - ATTR_FORKS: self._forks, - ATTR_HOMEPAGE: self._homepage, - ATTR_LATEST_COMMIT_MESSAGE: self._latest_commit_message, - ATTR_LATEST_COMMIT_SHA: self._latest_commit_sha, - ATTR_LATEST_OPEN_ISSUE_URL: self._latest_open_issue_url, - ATTR_LATEST_OPEN_PULL_REQUEST_URL: self._latest_open_pr_url, - ATTR_LATEST_RELEASE_TAG: self._latest_release_tag, - ATTR_LATEST_RELEASE_URL: self._latest_release_url, - ATTR_NAME: self._repo_name, - ATTR_OPEN_ISSUES: self._open_issues, - ATTR_OPEN_PULL_REQUESTS: self._pull_requests, - ATTR_STARGAZERS: self._stargazers, - ATTR_TOPICS: self._topics, - ATTR_VIEWS: self._views, - ATTR_VIEWS_UNIQUE: self._views_unique, - ATTR_WATCHERS: self._watchers, - } - async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" data: GitHubData = self._coordinator.data self._state = data.last_commit.sha_short - self._clones = data.clones.count - self._clones_unique = data.clones.count_uniques - self._description = data.repository.description - self._forks = data.repository.attributes.get("forks") - self._homepage = data.repository.attributes.get("homepage") - self._latest_commit_message = data.last_commit.message - self._latest_commit_sha = data.last_commit.sha - self._latest_open_issue_url = ( - data.open_issues[0].html_url if len(data.open_issues) > 1 else "" - ) - self._latest_open_pr_url = ( - data.open_pull_requests[0].html_url - if len(data.open_pull_requests) > 1 - else "" - ) - self._latest_release_tag = ( - data.releases[0].tag_name if len(data.releases) > 1 else "" - ) - self._latest_release_url = ( - f"https://github.com/{data.repository.full_name}/releases/{data.releases[0].tag_name}" - if len(data.releases) > 1 - else "" - ) - self._open_issues = len(data.open_issues) - self._pull_requests = len(data.open_pull_requests) - self._repo_name = data.repository.attributes.get("name") - self._stargazers = data.repository.attributes.get("stargazers_count") - self._topics = data.repository.topics - self._views = data.views.count - self._views_unique = data.views.count_uniques - self._watchers = data.repository.attributes.get("watchers_count") + self._attributes = { + ATTR_CLONES: data.clones.count, + ATTR_CLONES_UNIQUE: data.clones.count_uniques, + ATTR_DESCRIPTION: data.repository.description, + ATTR_FORKS: data.repository.attributes.get("forks"), + ATTR_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_LATEST_COMMIT_MESSAGE: data.last_commit.message, + ATTR_LATEST_COMMIT_SHA: data.last_commit.sha, + ATTR_LATEST_OPEN_ISSUE_URL: ( + data.open_issues[0].html_url if len(data.open_issues) > 1 else "" + ), + ATTR_LATEST_OPEN_PULL_REQUEST_URL: ( + data.open_pull_requests[0].html_url + if len(data.open_pull_requests) > 1 + else None + ), + ATTR_LATEST_RELEASE_TAG: ( + data.releases[0].tag_name if len(data.releases) > 1 else "" + ), + ATTR_LATEST_RELEASE_URL: ( + f"https://github.com/{data.repository.full_name}/releases/{data.releases[0].tag_name}" + if len(data.releases) > 1 + else None + ), + ATTR_OPEN_ISSUES: len(data.open_issues), + ATTR_OPEN_PULL_REQUESTS: len(data.open_pull_requests), + ATTR_NAME: data.repository.attributes.get("name"), + ATTR_PATH: data.repository.full_name, + ATTR_STARGAZERS: data.repository.attributes.get("stargazers_count"), + ATTR_TOPICS: data.repository.topics, + ATTR_VIEWS: data.views.count, + ATTR_VIEWS_UNIQUE: data.views.count_uniques, + ATTR_WATCHERS: data.repository.attributes.get("watchers_count"), + } return True From bc24053dd7d7a1bb346f23240f02d36c7023f262 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 03:51:19 +0100 Subject: [PATCH 12/43] Split sensor into individual sensors --- homeassistant/components/github/__init__.py | 11 +- homeassistant/components/github/sensor.py | 376 +++++++++++++++++--- 2 files changed, 329 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index a13fb7b2db155..2399a4af0437d 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -37,7 +37,7 @@ def __init__(self, count: int, count_uniques: int) -> None: self.count_uniques = count_uniques -class GitHubLastCommit: +class GitHubLatestCommit: """Represents a GitHub last commit object.""" def __init__(self, sha: int, message: str) -> None: @@ -62,7 +62,7 @@ class GitHubData: def __init__( self, repository: AIOGitHubAPIRepository, - last_commit: GitHubLastCommit, + last_commit: GitHubLatestCommit, clones: GitHubClones = None, issues: List[AIOGitHubAPIRepositoryIssue] = None, releases: List[AIOGitHubAPIRepositoryRelease] = None, @@ -117,7 +117,7 @@ async def async_update_data() -> GitHubData: repository: AIOGitHubAPIRepository = await github.get_repo( entry.data[CONF_REPOSITORY] ) - last_commit = await repository.client.get( + latest_commit = await repository.client.get( endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" ) clones = await repository.client.get( @@ -130,8 +130,9 @@ async def async_update_data() -> GitHubData: ) return GitHubData( repository, - GitHubLastCommit( - last_commit["commit"]["sha"], last_commit["commit"]["commit"]["message"] + GitHubLatestCommit( + latest_commit["commit"]["sha"], + latest_commit["commit"]["commit"]["message"], ), GitHubClones(clones["count"], clones["uniques"]), issues, diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index dee901841b802..211215554e350 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -4,7 +4,7 @@ from aiogithubapi.objects.repository import AIOGitHubAPIRepository -from homeassistant.const import ATTR_NAME +from homeassistant.const import ATTR_DATE, ATTR_ID, ATTR_NAME from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import GitHubData, GitHubDeviceEntity @@ -12,25 +12,25 @@ _LOGGER = logging.getLogger(__name__) + +ATTR_ASSIGNEES = "assignees" ATTR_CLONES = "clones" -ATTR_CLONES_UNIQUE = "clones_unique" -ATTR_DESCRIPTION = "description" -ATTR_FORKS = "forks" -ATTR_HOMEPAGE = "homepage" -ATTR_LATEST_COMMIT_MESSAGE = "latest_commit_message" -ATTR_LATEST_COMMIT_SHA = "latest_commit_sha" -ATTR_LATEST_OPEN_ISSUE_URL = "latest_open_issue_url" -ATTR_LATEST_OPEN_PULL_REQUEST_URL = "latest_open_pull_request_url" -ATTR_LATEST_RELEASE_TAG = "latest_release_tag" -ATTR_LATEST_RELEASE_URL = "latest_release_url" -ATTR_OPEN_ISSUES = "open_issues" -ATTR_OPEN_PULL_REQUESTS = "open_pull_requests" -ATTR_PATH = "path" -ATTR_STARGAZERS = "stargazers" -ATTR_TOPICS = "topics" -ATTR_VIEWS = "views" -ATTR_VIEWS_UNIQUE = "views_unique" -ATTR_WATCHERS = "watchers" +ATTR_DRAFT = "draft" +ATTR_LABELS = "labels" +ATTR_MESSAGE = "message" +ATTR_NUMBER = "number" +ATTR_OPEN = "open" +ATTR_PRERELEASE = "prerelease" +ATTR_RELEASES = "releases" +ATTR_REPO_DESCRIPTION = "repository_description" +ATTR_REPO_HOMEPAGE = "repository_homepage" +ATTR_REPO_NAME = "repository_name" +ATTR_REPO_PATH = "repository_path" +ATTR_REPO_TOPICS = "repository_topics" +ATTR_SHA = "sha" +ATTR_UNIQUE = "unique" +ATTR_URL = "url" +ATTR_USER = "user" SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 @@ -45,7 +45,20 @@ async def async_setup_entry(hass, entry, async_add_entities): DATA_REPOSITORY ] - async_add_entities([LastCommitSensor(coordinator, repository)], True) + async_add_entities( + [ + ClonesSensor(coordinator, repository), + ForksSensor(coordinator, repository), + LatestCommitSensor(coordinator, repository), + LatestOpenIssueSensor(coordinator, repository), + LatestPullRequestSensor(coordinator, repository), + LatestReleaseSensor(coordinator, repository), + StargazersSensor(coordinator, repository), + ViewsSensor(coordinator, repository), + WatchersSensor(coordinator, repository), + ], + True, + ) class GitHubSensor(GitHubDeviceEntity): @@ -76,7 +89,66 @@ def device_state_attributes(self) -> object: return self._attributes -class LastCommitSensor(GitHubSensor): +class ClonesSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, repository, "clones", f"{name} Clones", "mdi:github" + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + self._state = data.clones.count + + self._attributes = { + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + ATTR_UNIQUE: data.clones.count_uniques, + } + + return True + + +class ForksSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, repository, "forks", f"{name} Forks", "mdi:github" + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + self._state = data.repository.attributes.get("forks") + + self._attributes = { + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + } + + return True + + +class LatestCommitSensor(GitHubSensor): """Representation of a Repository Sensor.""" def __init__( @@ -95,38 +167,236 @@ async def _github_update(self) -> bool: self._state = data.last_commit.sha_short self._attributes = { - ATTR_CLONES: data.clones.count, - ATTR_CLONES_UNIQUE: data.clones.count_uniques, - ATTR_DESCRIPTION: data.repository.description, - ATTR_FORKS: data.repository.attributes.get("forks"), - ATTR_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_LATEST_COMMIT_MESSAGE: data.last_commit.message, - ATTR_LATEST_COMMIT_SHA: data.last_commit.sha, - ATTR_LATEST_OPEN_ISSUE_URL: ( - data.open_issues[0].html_url if len(data.open_issues) > 1 else "" - ), - ATTR_LATEST_OPEN_PULL_REQUEST_URL: ( - data.open_pull_requests[0].html_url - if len(data.open_pull_requests) > 1 - else None - ), - ATTR_LATEST_RELEASE_TAG: ( - data.releases[0].tag_name if len(data.releases) > 1 else "" - ), - ATTR_LATEST_RELEASE_URL: ( - f"https://github.com/{data.repository.full_name}/releases/{data.releases[0].tag_name}" - if len(data.releases) > 1 - else None - ), - ATTR_OPEN_ISSUES: len(data.open_issues), - ATTR_OPEN_PULL_REQUESTS: len(data.open_pull_requests), - ATTR_NAME: data.repository.attributes.get("name"), - ATTR_PATH: data.repository.full_name, - ATTR_STARGAZERS: data.repository.attributes.get("stargazers_count"), - ATTR_TOPICS: data.repository.topics, - ATTR_VIEWS: data.views.count, - ATTR_VIEWS_UNIQUE: data.views.count_uniques, - ATTR_WATCHERS: data.repository.attributes.get("watchers_count"), + ATTR_MESSAGE: data.last_commit.message, + ATTR_SHA: data.last_commit.sha, + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + } + + return True + + +class LatestOpenIssueSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, + repository, + "latest_open_issue", + f"{name} Latest Open Issue", + "mdi:github", + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + if data.open_issues is None or len(data.open_issues) < 1: + return False + + self._state = data.open_issues[0].title + + labels = [] + for label in data.open_issues[0].labels: + labels.append(label.get("name")) + + self._attributes = { + ATTR_ASSIGNEES: data.open_issues[0].assignees, + ATTR_ID: data.open_issues[0].id, + ATTR_LABELS: labels, + ATTR_NUMBER: data.open_issues[0].number, + ATTR_OPEN: len(data.open_issues), + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + ATTR_URL: data.open_issues[0].html_url, + ATTR_USER: data.open_issues[0].user.login, + } + + return True + + +class LatestPullRequestSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, + repository, + "latest_pull_request", + f"{name} Latest Pull Request", + "mdi:github", + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + if data.open_pull_requests is None or len(data.open_pull_requests) < 1: + return False + + self._state = data.open_pull_requests[0].title + + labels = [] + for label in data.open_pull_requests[0].labels: + labels.append(label.get("name")) + + self._attributes = { + ATTR_ASSIGNEES: data.open_pull_requests[0].assignees, + ATTR_ID: data.open_pull_requests[0].id, + ATTR_LABELS: labels, + ATTR_NUMBER: data.open_pull_requests[0].number, + ATTR_OPEN: len(data.open_pull_requests), + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + ATTR_USER: data.open_pull_requests[0].user.login, + } + + return True + + +class LatestReleaseSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, + repository, + "latest_release", + f"{name} Latest Release", + "mdi:github", + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + if data.releases is None or len(data.releases) < 1: + return False + + self._state = data.releases[0].tag_name + + self._attributes = { + ATTR_DATE: data.releases[0].published_at, + ATTR_DRAFT: data.releases[0].draft, + ATTR_NAME: data.releases[0].name, + ATTR_PRERELEASE: data.releases[0].prerelease, + ATTR_RELEASES: len(data.releases), + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + ATTR_URL: f"https://github.com/{data.repository.full_name}/releases/{data.releases[0].tag_name}", + } + + return True + + +class StargazersSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, repository, "stargazers", f"{name} Stargazers", "mdi:github" + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + self._state = data.repository.attributes.get("stargazers_count") + + self._attributes = { + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + } + + return True + + +class ViewsSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, repository, "views", f"{name} Views", "mdi:github" + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + self._state = data.views.count + + self._attributes = { + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, + ATTR_UNIQUE: data.views.count_uniques, + } + + return True + + +class WatchersSensor(GitHubSensor): + """Representation of a Repository Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository + ) -> None: + """Initialize the sensor.""" + name = repository.attributes.get("name") + super().__init__( + coordinator, repository, "watchers", f"{name} Watchers", "mdi:github" + ) + + async def _github_update(self) -> bool: + """Fetch new state data for the sensor.""" + data: GitHubData = self._coordinator.data + + self._state = data.repository.attributes.get("watchers_count") + + self._attributes = { + ATTR_REPO_DESCRIPTION: data.repository.description, + ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), + ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_PATH: data.repository.full_name, + ATTR_REPO_TOPICS: data.repository.topics, } return True From e149cb0a10bc1b0b0afff2f3db1444852a430dae Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 03:59:49 +0100 Subject: [PATCH 13/43] Add TODOs --- homeassistant/components/github/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 2399a4af0437d..3aba1006bce0c 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,4 +1,7 @@ """The GitHub integration.""" +# TODO: Disable extra REST calls by default using option flow +# TODO: Tests +# TODO: Device triggers? (scaffold) import asyncio from datetime import timedelta import logging @@ -111,7 +114,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) return False - # TODO: Disable extra REST calls by default using option flow async def async_update_data() -> GitHubData: """Fetch data from GitHub.""" repository: AIOGitHubAPIRepository = await github.get_repo( From f1ff436409c458905dcb5f1e8b7d52255b85424d Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 04:01:19 +0100 Subject: [PATCH 14/43] Add TODO --- homeassistant/components/github/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 3aba1006bce0c..90c61bc5d3b37 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -2,6 +2,7 @@ # TODO: Disable extra REST calls by default using option flow # TODO: Tests # TODO: Device triggers? (scaffold) +# TODO: Breaking changes (flow, url, options flow, split etc.) import asyncio from datetime import timedelta import logging From 5aaf11c313222614adb0f2e0604080032600927e Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 04:06:00 +0100 Subject: [PATCH 15/43] Change name --- homeassistant/components/github/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index a7ea73f007121..54c1c3e215bdf 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -40,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, data): except AIOGitHubAPIException: raise CannotConnect - return {"title": repository.full_name} + return {"title": repository.attributes.get("name")} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): From 6d8eddf3d8f6b530c5d82795a2b72704e83dfd54 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 04:34:23 +0100 Subject: [PATCH 16/43] Fix for clones and views for those without push access --- homeassistant/components/github/__init__.py | 25 ++++++++++++------ homeassistant/components/github/sensor.py | 28 ++++++++++----------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 90c61bc5d3b37..3e61c26358994 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -123,24 +123,33 @@ async def async_update_data() -> GitHubData: latest_commit = await repository.client.get( endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" ) - clones = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/clones" - ) issues: [AIOGitHubAPIRepositoryIssue] = await repository.get_issues() releases: [AIOGitHubAPIRepositoryRelease] = await repository.get_releases() - views = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/views" - ) + if repository.attributes.get("permissions").get("push") is True: + clones = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/clones" + ) + views = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/views" + ) + else: + clones = None + views = None + return GitHubData( repository, GitHubLatestCommit( latest_commit["commit"]["sha"], latest_commit["commit"]["commit"]["message"], ), - GitHubClones(clones["count"], clones["uniques"]), + GitHubClones(clones["count"], clones["uniques"]) + if clones is not None + else None, issues, releases, - GitHubViews(views["count"], views["uniques"]), + GitHubViews(views["count"], views["uniques"]) + if views is not None + else None, ) coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 211215554e350..7051b6c9ad17d 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -45,20 +45,20 @@ async def async_setup_entry(hass, entry, async_add_entities): DATA_REPOSITORY ] - async_add_entities( - [ - ClonesSensor(coordinator, repository), - ForksSensor(coordinator, repository), - LatestCommitSensor(coordinator, repository), - LatestOpenIssueSensor(coordinator, repository), - LatestPullRequestSensor(coordinator, repository), - LatestReleaseSensor(coordinator, repository), - StargazersSensor(coordinator, repository), - ViewsSensor(coordinator, repository), - WatchersSensor(coordinator, repository), - ], - True, - ) + sensors = [ + ForksSensor(coordinator, repository), + LatestCommitSensor(coordinator, repository), + LatestOpenIssueSensor(coordinator, repository), + LatestPullRequestSensor(coordinator, repository), + LatestReleaseSensor(coordinator, repository), + StargazersSensor(coordinator, repository), + WatchersSensor(coordinator, repository), + ] + if repository.attributes.get("permissions").get("push") is True: + sensors.append(ClonesSensor(coordinator, repository)) + sensors.append(ViewsSensor(coordinator, repository)) + + async_add_entities(sensors, True) class GitHubSensor(GitHubDeviceEntity): From 253e513d700681c4ac740f9aa332a3de44319fa9 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 16:35:34 +0100 Subject: [PATCH 17/43] Options flow --- homeassistant/components/github/__init__.py | 69 ++++++++++---- .../components/github/config_flow.py | 74 ++++++++++++++- homeassistant/components/github/const.py | 5 + homeassistant/components/github/sensor.py | 93 ++++++++++++++----- homeassistant/components/github/strings.json | 22 ++++- .../components/github/translations/en.json | 22 ++++- 6 files changed, 237 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 3e61c26358994..10244051f7860 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,5 +1,4 @@ """The GitHub integration.""" -# TODO: Disable extra REST calls by default using option flow # TODO: Tests # TODO: Device triggers? (scaffold) # TODO: Breaking changes (flow, url, options flow, split etc.) @@ -22,10 +21,21 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_REPOSITORY, DATA_COORDINATOR, DATA_REPOSITORY, DOMAIN +from .const import ( + CONF_CLONES, + CONF_ISSUES_PRS, + CONF_LATEST_COMMIT, + CONF_LATEST_RELEASE, + CONF_REPOSITORY, + CONF_VIEWS, + DATA_COORDINATOR, + DATA_REPOSITORY, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -66,7 +76,7 @@ class GitHubData: def __init__( self, repository: AIOGitHubAPIRepository, - last_commit: GitHubLatestCommit, + latest_commit: GitHubLatestCommit = None, clones: GitHubClones = None, issues: List[AIOGitHubAPIRepositoryIssue] = None, releases: List[AIOGitHubAPIRepositoryRelease] = None, @@ -74,7 +84,7 @@ def __init__( ) -> None: """Initialize the GitHub data object.""" self.repository = repository - self.last_commit = last_commit + self.latest_commit = latest_commit self.clones = clones self.issues = issues self.releases = releases @@ -117,31 +127,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data() -> GitHubData: """Fetch data from GitHub.""" + _LOGGER.warning("async_update_data") repository: AIOGitHubAPIRepository = await github.get_repo( entry.data[CONF_REPOSITORY] ) - latest_commit = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" - ) - issues: [AIOGitHubAPIRepositoryIssue] = await repository.get_issues() - releases: [AIOGitHubAPIRepositoryRelease] = await repository.get_releases() - if repository.attributes.get("permissions").get("push") is True: - clones = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/clones" - ) - views = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/views" + if entry.options.get(CONF_LATEST_COMMIT, True) is True: + latest_commit = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" ) + else: + latest_commit = None + if entry.options.get(CONF_ISSUES_PRS, False) is True: + issues: List[AIOGitHubAPIRepositoryIssue] = await repository.get_issues() + else: + issues = None + if entry.options.get(CONF_LATEST_RELEASE, False) is True: + releases: List[ + AIOGitHubAPIRepositoryRelease + ] = await repository.get_releases() + else: + releases = None + if repository.attributes.get("permissions").get("push") is True: + if entry.options.get(CONF_CLONES, False) is True: + clones = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/clones" + ) + else: + clones = None + if entry.options.get(CONF_VIEWS, False) is True: + views = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/views" + ) + else: + views = None else: clones = None views = None + _LOGGER.warning(repository.full_name) + return GitHubData( repository, GitHubLatestCommit( latest_commit["commit"]["sha"], latest_commit["commit"]["commit"]["message"], - ), + ) + if latest_commit is not None + else None, GitHubClones(clones["count"], clones["uniques"]) if clones is not None else None, @@ -170,6 +202,9 @@ async def async_update_data() -> GitHubData: # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -234,6 +269,8 @@ def should_poll(self): async def async_update(self) -> None: """Update GitHub entity.""" + _LOGGER.warning("async_update") + if await self._github_update(): self._available = True else: diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 54c1c3e215bdf..cf106ff02f615 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -11,8 +11,17 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_ACCESS_TOKEN - -from .const import CONF_REPOSITORY, DOMAIN # pylint: disable=unused-import +from homeassistant.core import callback + +from .const import ( + CONF_CLONES, + CONF_ISSUES_PRS, + CONF_LATEST_COMMIT, + CONF_LATEST_RELEASE, + CONF_REPOSITORY, + CONF_VIEWS, +) +from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -96,7 +105,7 @@ async def async_step_reauth(self, user_input): self.context = {} # pylint: disable=no-member self.context["title_placeholders"] = { - "name": user_input[CONF_REPOSITORY], + "repository": user_input[CONF_REPOSITORY], } try: await validate_input(self.hass, user_input) @@ -120,6 +129,65 @@ async def async_step_reauth(self, user_input): step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors ) + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Options callback for GitHub.""" + return GitHubOptionsFlowHandler(config_entry) + + +class GitHubOptionsFlowHandler(config_entries.OptionsFlow): + """Config flow options for GitHub.""" + + def __init__(self, config_entry): + """Initialize GitHub options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + try: + github = GitHub(self.config_entry.data[CONF_ACCESS_TOKEN]) + repository = await github.get_repo(self.config_entry.data[CONF_REPOSITORY]) + except (AIOGitHubAPIAuthenticationException, AIOGitHubAPIException): + return self.async_abort(reason="connection_error") + + schema = { + vol.Optional( + CONF_LATEST_COMMIT, + default=self.config_entry.options.get(CONF_LATEST_COMMIT, True), + ): bool, + vol.Optional( + CONF_LATEST_RELEASE, + default=self.config_entry.options.get(CONF_LATEST_RELEASE, False), + ): bool, + vol.Optional( + CONF_ISSUES_PRS, + default=self.config_entry.options.get(CONF_ISSUES_PRS, False), + ): bool, + } + + if repository.attributes.get("permissions").get("push") is True: + schema = { + vol.Optional( + CONF_CLONES, + default=self.config_entry.options.get(CONF_CLONES, False), + ): bool, + **schema, + vol.Optional( + CONF_VIEWS, + default=self.config_entry.options.get(CONF_VIEWS, False), + ): bool, + } + + return self.async_show_form(step_id="user", data_schema=vol.Schema(schema),) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index 44f0c40513b82..6f888468e62cb 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -5,4 +5,9 @@ DOMAIN = "github" +CONF_CLONES = "clones" +CONF_ISSUES_PRS = "issues_and_prs" +CONF_LATEST_COMMIT = "latest_commit" +CONF_LATEST_RELEASE = "latest_release" CONF_REPOSITORY = "repository" +CONF_VIEWS = "views" diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 7051b6c9ad17d..9b58e3e9a0e3d 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,14 +1,27 @@ """Sensor platform for GitHub integration.""" -from datetime import timedelta import logging from aiogithubapi.objects.repository import AIOGitHubAPIRepository from homeassistant.const import ATTR_DATE, ATTR_ID, ATTR_NAME +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import GitHubData, GitHubDeviceEntity -from .const import DATA_COORDINATOR, DATA_REPOSITORY, DOMAIN +from .const import ( + CONF_CLONES, + CONF_ISSUES_PRS, + CONF_LATEST_COMMIT, + CONF_LATEST_RELEASE, + CONF_VIEWS, + DATA_COORDINATOR, + DATA_REPOSITORY, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -32,9 +45,6 @@ ATTR_URL = "url" ATTR_USER = "user" -SCAN_INTERVAL = timedelta(seconds=300) -PARALLEL_UPDATES = 4 - async def async_setup_entry(hass, entry, async_add_entities): """Set up the sensor platform.""" @@ -45,20 +55,49 @@ async def async_setup_entry(hass, entry, async_add_entities): DATA_REPOSITORY ] - sensors = [ - ForksSensor(coordinator, repository), - LatestCommitSensor(coordinator, repository), - LatestOpenIssueSensor(coordinator, repository), - LatestPullRequestSensor(coordinator, repository), - LatestReleaseSensor(coordinator, repository), - StargazersSensor(coordinator, repository), - WatchersSensor(coordinator, repository), - ] - if repository.attributes.get("permissions").get("push") is True: - sensors.append(ClonesSensor(coordinator, repository)) - sensors.append(ViewsSensor(coordinator, repository)) - - async_add_entities(sensors, True) + @callback + def add_sensor_entities(): + """Add sensor entities.""" + sensors = [ + ForksSensor(coordinator, repository), + StargazersSensor(coordinator, repository), + WatchersSensor(coordinator, repository), + ] + if ( + entry.options.get(CONF_CLONES, False) is True + and repository.attributes.get("permissions").get("push") is True + ): + sensors.append(ClonesSensor(coordinator, repository)) + if entry.options.get(CONF_LATEST_COMMIT, True) is True: + sensors.append(LatestCommitSensor(coordinator, repository)) + if entry.options.get(CONF_ISSUES_PRS, False) is True: + sensors.append(LatestOpenIssueSensor(coordinator, repository)) + sensors.append(LatestPullRequestSensor(coordinator, repository)) + if entry.options.get(CONF_LATEST_RELEASE, False) is True: + sensors.append(LatestReleaseSensor(coordinator, repository)) + if ( + entry.options.get(CONF_VIEWS, False) is True + and repository.attributes.get("permissions").get("push") is True + ): + sensors.append(ViewsSensor(coordinator, repository)) + + _LOGGER.warning("Add Sensors") + _LOGGER.warning(sensors) + + async_add_entities(sensors, False) + + async_dispatcher_connect( + hass, f"signal-{DOMAIN}-sensors-update-{entry.entry_id}", add_sensor_entities + ) + + entry.add_update_listener(async_config_entry_updated) + + add_sensor_entities() + + +async def async_config_entry_updated(hass, entry) -> None: + """Handle signals of config entry being updated.""" + async_dispatcher_send(hass, f"signal-{DOMAIN}-sensors-update-{entry.entry_id}") class GitHubSensor(GitHubDeviceEntity): @@ -76,6 +115,8 @@ def __init__( self._state = None self._attributes = None + _LOGGER.warning("init: %s", name) + super().__init__(coordinator, f"{repository.full_name}_{unique_id}", name, icon) @property @@ -135,6 +176,8 @@ async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" data: GitHubData = self._coordinator.data + _LOGGER.warning("Update Forks") + self._state = data.repository.attributes.get("forks") self._attributes = { @@ -157,18 +200,22 @@ def __init__( """Initialize the sensor.""" name = repository.attributes.get("name") super().__init__( - coordinator, repository, "last_commit", f"{name} Last Commit", "mdi:github" + coordinator, + repository, + "latest_commit", + f"{name} Latest Commit", + "mdi:github", ) async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" data: GitHubData = self._coordinator.data - self._state = data.last_commit.sha_short + self._state = data.latest_commit.sha_short self._attributes = { - ATTR_MESSAGE: data.last_commit.message, - ATTR_SHA: data.last_commit.sha, + ATTR_MESSAGE: data.latest_commit.message, + ATTR_SHA: data.latest_commit.sha, ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), ATTR_REPO_NAME: data.repository.attributes.get("name"), diff --git a/homeassistant/components/github/strings.json b/homeassistant/components/github/strings.json index 4f68f8f03642b..6dd2ab25678a2 100644 --- a/homeassistant/components/github/strings.json +++ b/homeassistant/components/github/strings.json @@ -4,14 +4,14 @@ "flow_title": "GitHub: {repository}", "step": { "reauth": { - "title": "GitHub: {repository}", - "description": "Please re-enter your access token.", + "title": "GitHub: Reauthentication", + "description": "Please re-enter your [access token](https://github.com/settings/tokens).", "data": { "access_token": "Access Token" } }, "user": { - "description": "Please enter your access token and repository.", + "description": "Please enter your [access token](https://github.com/settings/tokens) and repository (`org/project`).", "data": { "access_token": "Access Token", "repository": "Repository" @@ -26,7 +26,23 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "connection_error": "There was an error connecting to GitHub.", "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully" } + }, + "options": { + "step": { + "user": { + "title": "GitHub Options", + "description": "To help reduce the chances of hitting API limits, some items have been disabled by default. You can enable and disable these items here.", + "data": { + "clones": "Clones", + "issues_and_prs": "Issues and Pull Requests", + "latest_commit": "Latest Commit", + "latest_release": "Latest Release", + "views": "Views" + } + } + } } } diff --git a/homeassistant/components/github/translations/en.json b/homeassistant/components/github/translations/en.json index 4f68f8f03642b..6dd2ab25678a2 100644 --- a/homeassistant/components/github/translations/en.json +++ b/homeassistant/components/github/translations/en.json @@ -4,14 +4,14 @@ "flow_title": "GitHub: {repository}", "step": { "reauth": { - "title": "GitHub: {repository}", - "description": "Please re-enter your access token.", + "title": "GitHub: Reauthentication", + "description": "Please re-enter your [access token](https://github.com/settings/tokens).", "data": { "access_token": "Access Token" } }, "user": { - "description": "Please enter your access token and repository.", + "description": "Please enter your [access token](https://github.com/settings/tokens) and repository (`org/project`).", "data": { "access_token": "Access Token", "repository": "Repository" @@ -26,7 +26,23 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "connection_error": "There was an error connecting to GitHub.", "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully" } + }, + "options": { + "step": { + "user": { + "title": "GitHub Options", + "description": "To help reduce the chances of hitting API limits, some items have been disabled by default. You can enable and disable these items here.", + "data": { + "clones": "Clones", + "issues_and_prs": "Issues and Pull Requests", + "latest_commit": "Latest Commit", + "latest_release": "Latest Release", + "views": "Views" + } + } + } } } From 9dfa2817bc94ab46ef66e388b8c347f2e8511b94 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 16:45:11 +0100 Subject: [PATCH 18/43] Cleanup and fix update --- homeassistant/components/github/__init__.py | 5 ----- homeassistant/components/github/sensor.py | 9 +-------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 10244051f7860..7cadb491bae15 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -127,7 +127,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data() -> GitHubData: """Fetch data from GitHub.""" - _LOGGER.warning("async_update_data") repository: AIOGitHubAPIRepository = await github.get_repo( entry.data[CONF_REPOSITORY] ) @@ -164,8 +163,6 @@ async def async_update_data() -> GitHubData: clones = None views = None - _LOGGER.warning(repository.full_name) - return GitHubData( repository, GitHubLatestCommit( @@ -269,8 +266,6 @@ def should_poll(self): async def async_update(self) -> None: """Update GitHub entity.""" - _LOGGER.warning("async_update") - if await self._github_update(): self._available = True else: diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 9b58e3e9a0e3d..3998c8c183b72 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -81,10 +81,7 @@ def add_sensor_entities(): ): sensors.append(ViewsSensor(coordinator, repository)) - _LOGGER.warning("Add Sensors") - _LOGGER.warning(sensors) - - async_add_entities(sensors, False) + async_add_entities(sensors, True) async_dispatcher_connect( hass, f"signal-{DOMAIN}-sensors-update-{entry.entry_id}", add_sensor_entities @@ -115,8 +112,6 @@ def __init__( self._state = None self._attributes = None - _LOGGER.warning("init: %s", name) - super().__init__(coordinator, f"{repository.full_name}_{unique_id}", name, icon) @property @@ -176,8 +171,6 @@ async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" data: GitHubData = self._coordinator.data - _LOGGER.warning("Update Forks") - self._state = data.repository.attributes.get("forks") self._attributes = { From 145dbf75159754368b8f9d91c4cb592030aca4bd Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 26 Jul 2020 17:33:29 +0100 Subject: [PATCH 19/43] Update and handle errors --- homeassistant/components/github/__init__.py | 119 +++++++++++--------- homeassistant/components/github/sensor.py | 17 ++- 2 files changed, 81 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 7cadb491bae15..a5ad492ddd605 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -17,13 +17,14 @@ AIOGitHubAPIRepositoryIssue, AIOGitHubAPIRepositoryRelease, ) +import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( CONF_CLONES, @@ -127,59 +128,65 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data() -> GitHubData: """Fetch data from GitHub.""" - repository: AIOGitHubAPIRepository = await github.get_repo( - entry.data[CONF_REPOSITORY] - ) - if entry.options.get(CONF_LATEST_COMMIT, True) is True: - latest_commit = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" - ) - else: - latest_commit = None - if entry.options.get(CONF_ISSUES_PRS, False) is True: - issues: List[AIOGitHubAPIRepositoryIssue] = await repository.get_issues() - else: - issues = None - if entry.options.get(CONF_LATEST_RELEASE, False) is True: - releases: List[ - AIOGitHubAPIRepositoryRelease - ] = await repository.get_releases() - else: - releases = None - if repository.attributes.get("permissions").get("push") is True: - if entry.options.get(CONF_CLONES, False) is True: - clones = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/clones" + try: + async with async_timeout.timeout(60): + repository: AIOGitHubAPIRepository = await github.get_repo( + entry.data[CONF_REPOSITORY] ) - else: - clones = None - if entry.options.get(CONF_VIEWS, False) is True: - views = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/views" + if entry.options.get(CONF_LATEST_COMMIT, True) is True: + latest_commit = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" + ) + else: + latest_commit = None + if entry.options.get(CONF_ISSUES_PRS, False) is True: + issues: List[ + AIOGitHubAPIRepositoryIssue + ] = await repository.get_issues() + else: + issues = None + if entry.options.get(CONF_LATEST_RELEASE, False) is True: + releases: List[ + AIOGitHubAPIRepositoryRelease + ] = await repository.get_releases() + else: + releases = None + if repository.attributes.get("permissions").get("push") is True: + if entry.options.get(CONF_CLONES, False) is True: + clones = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/clones" + ) + else: + clones = None + if entry.options.get(CONF_VIEWS, False) is True: + views = await repository.client.get( + endpoint=f"/repos/{repository.full_name}/traffic/views" + ) + else: + views = None + else: + clones = None + views = None + + return GitHubData( + repository, + GitHubLatestCommit( + latest_commit["commit"]["sha"], + latest_commit["commit"]["commit"]["message"], + ) + if latest_commit is not None + else None, + GitHubClones(clones["count"], clones["uniques"]) + if clones is not None + else None, + issues, + releases, + GitHubViews(views["count"], views["uniques"]) + if views is not None + else None, ) - else: - views = None - else: - clones = None - views = None - - return GitHubData( - repository, - GitHubLatestCommit( - latest_commit["commit"]["sha"], - latest_commit["commit"]["commit"]["message"], - ) - if latest_commit is not None - else None, - GitHubClones(clones["count"], clones["uniques"]) - if clones is not None - else None, - issues, - releases, - GitHubViews(views["count"], views["uniques"]) - if views is not None - else None, - ) + except (AIOGitHubAPIAuthenticationException, AIOGitHubAPIException) as err: + raise UpdateFailed(f"Error communicating with GitHub: {err}") coordinator = DataUpdateCoordinator( hass, @@ -188,7 +195,7 @@ async def async_update_data() -> GitHubData: name=DOMAIN, update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=300), + update_interval=timedelta(seconds=120), ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { @@ -264,6 +271,12 @@ def should_poll(self): """No need to poll. Coordinator notifies entity of updates.""" return False + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + async def async_update(self) -> None: """Update GitHub entity.""" if await self._github_update(): diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 3998c8c183b72..b8635ba9078f2 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -45,6 +45,8 @@ ATTR_URL = "url" ATTR_USER = "user" +PARALLEL_UPDATES = 4 + async def async_setup_entry(hass, entry, async_add_entities): """Set up the sensor platform.""" @@ -56,8 +58,10 @@ async def async_setup_entry(hass, entry, async_add_entities): ] @callback - def add_sensor_entities(): + async def add_sensor_entities(): """Add sensor entities.""" + await coordinator.async_refresh() + sensors = [ ForksSensor(coordinator, repository), StargazersSensor(coordinator, repository), @@ -89,7 +93,7 @@ def add_sensor_entities(): entry.add_update_listener(async_config_entry_updated) - add_sensor_entities() + await add_sensor_entities() async def async_config_entry_updated(hass, entry) -> None: @@ -139,6 +143,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data self._state = data.clones.count @@ -169,6 +174,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data self._state = data.repository.attributes.get("forks") @@ -202,6 +208,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data self._state = data.latest_commit.sha_short @@ -237,6 +244,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data if data.open_issues is None or len(data.open_issues) < 1: @@ -284,6 +292,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data if data.open_pull_requests is None or len(data.open_pull_requests) < 1: @@ -330,6 +339,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data if data.releases is None or len(data.releases) < 1: @@ -368,6 +378,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data self._state = data.repository.attributes.get("stargazers_count") @@ -397,6 +408,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data self._state = data.views.count @@ -427,6 +439,7 @@ def __init__( async def _github_update(self) -> bool: """Fetch new state data for the sensor.""" + await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data self._state = data.repository.attributes.get("watchers_count") From 8fffd53e36be465709c415be4280cf0551d9b376 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 1 Aug 2020 13:27:58 +0100 Subject: [PATCH 20/43] Unique ids --- homeassistant/components/github/config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index cf106ff02f615..14339aea91110 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -76,9 +76,9 @@ async def async_step_user(self, user_input=None): errors["base"] = "cannot_find_repo" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + + await self.async_set_unique_id(user_input[CONF_REPOSITORY]) + self._abort_if_unique_id_configured() return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors @@ -107,6 +107,9 @@ async def async_step_reauth(self, user_input): self.context["title_placeholders"] = { "repository": user_input[CONF_REPOSITORY], } + + await self.async_set_unique_id(self.repository) + try: await validate_input(self.hass, user_input) for entry in self._async_current_entries(): @@ -121,9 +124,6 @@ async def async_step_reauth(self, user_input): errors["base"] = "cannot_find_repo" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" return self.async_show_form( step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors From 5ac5c282ede7aa15834b37e71adca9deae55289c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 1 Aug 2020 13:42:58 +0100 Subject: [PATCH 21/43] Fix unique id set position --- homeassistant/components/github/config_flow.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 14339aea91110..b89ec2febe783 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -69,6 +69,8 @@ async def async_step_user(self, user_input=None): if user_input is not None: try: info = await validate_input(self.hass, user_input) + await self.async_set_unique_id(user_input[CONF_REPOSITORY]) + self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: errors["base"] = "cannot_connect" @@ -77,9 +79,6 @@ async def async_step_user(self, user_input=None): except InvalidAuth: errors["base"] = "invalid_auth" - await self.async_set_unique_id(user_input[CONF_REPOSITORY]) - self._abort_if_unique_id_configured() - return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) From 164a37c67661b6f30fe31abc49876a20c046d5c6 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 1 Aug 2020 14:01:25 +0100 Subject: [PATCH 22/43] Add tests --- homeassistant/components/github/__init__.py | 1 - tests/components/github/test_config_flow.py | 123 +++++++++++++------- 2 files changed, 83 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index a5ad492ddd605..d89ed45751208 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,5 +1,4 @@ """The GitHub integration.""" -# TODO: Tests # TODO: Device triggers? (scaffold) # TODO: Breaking changes (flow, url, options flow, split etc.) import asyncio diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 020ea79ce7cc6..5476dfa976a20 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -1,90 +1,133 @@ """Test the GitHub config flow.""" -from homeassistant import config_entries, setup -from homeassistant.components.github.config_flow import CannotConnect, InvalidAuth -from homeassistant.components.github.const import DOMAIN +from aiogithubapi import AIOGitHubAPIAuthenticationException, AIOGitHubAPIException +from aiogithubapi.objects.repository import AIOGitHubAPIRepository + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.github.const import CONF_REPOSITORY, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +FIXTURE_REAUTH_INPUT = {CONF_ACCESS_TOKEN: "abc234"} +FIXTURE_USER_INPUT = {CONF_ACCESS_TOKEN: "abc123", CONF_REPOSITORY: "test/repo"} +UNIQUE_ID = "test/repo" -async def test_form(hass): + +async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( - "homeassistant.components.github.config_flow.PlaceholderHub.authenticate", - return_value=True, + "homeassistant.components.github.config_flow.GitHub.get_repo", + return_value=AIOGitHubAPIRepository(aioclient_mock, {"name": "repo"}), ), patch( "homeassistant.components.github.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.github.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - }, + result["flow_id"], FIXTURE_USER_INPUT ) - assert result2["type"] == "create_entry" - assert result2["title"] == "Name of the device" - assert result2["data"] == { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - } + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "repo" + assert result2["data"] == FIXTURE_USER_INPUT await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth(hass): +async def test_form_invalid_auth(hass: HomeAssistant): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.github.config_flow.PlaceholderHub.authenticate", - side_effect=InvalidAuth, + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIAuthenticationException, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - }, + result["flow_id"], FIXTURE_USER_INPUT ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass): +async def test_form_cannot_connect(hass: HomeAssistant): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.github.config_flow.PlaceholderHub.authenticate", - side_effect=CannotConnect, + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIException, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - }, + result["flow_id"], FIXTURE_USER_INPUT ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_cannot_find_repo(hass: HomeAssistant): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_find_repo"} + + +async def test_reauth_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test we get the reauth form.""" + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIAuthenticationException, + ): + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "invalid_auth"} + + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + return_value=AIOGitHubAPIRepository(aioclient_mock, {"name": "repo"}), + ), patch("homeassistant.components.github.async_setup", return_value=True), patch( + "homeassistant.components.github.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_REAUTH_INPUT + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" From e783a396f7196a7b1737a579d113733749de5d5a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 1 Aug 2020 14:52:34 +0100 Subject: [PATCH 23/43] Add untested to coverage --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index b33d9b15f991a..53fdefec1e0c6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -313,6 +313,11 @@ omit = homeassistant/components/geniushub/* homeassistant/components/geizhals/sensor.py homeassistant/components/github/sensor.py + homeassistant/components/gios/__init__.py + homeassistant/components/gios/air_quality.py + homeassistant/components/azure_devops/__init__.py + homeassistant/components/azure_devops/const.py + homeassistant/components/azure_devops/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py homeassistant/components/glances/__init__.py From 550801cdfced3f19c0c7defcbbebf8a30a5419ba Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 1 Aug 2020 14:54:43 +0100 Subject: [PATCH 24/43] Cleanup --- homeassistant/components/github/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index d89ed45751208..a0d076a9c96f8 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,6 +1,4 @@ """The GitHub integration.""" -# TODO: Device triggers? (scaffold) -# TODO: Breaking changes (flow, url, options flow, split etc.) import asyncio from datetime import timedelta import logging From 96e564ee3e315ddcdd7dd81bcebf08061e6b1824 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 1 Aug 2020 15:57:12 +0100 Subject: [PATCH 25/43] Fix typo --- .coveragerc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 53fdefec1e0c6..75b771159faa3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -315,9 +315,9 @@ omit = homeassistant/components/github/sensor.py homeassistant/components/gios/__init__.py homeassistant/components/gios/air_quality.py - homeassistant/components/azure_devops/__init__.py - homeassistant/components/azure_devops/const.py - homeassistant/components/azure_devops/sensor.py + homeassistant/components/github/__init__.py + homeassistant/components/github/const.py + homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py homeassistant/components/glances/__init__.py From c205dfa407246074380e227bca30ecedb32b8d7c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 14:20:00 +0100 Subject: [PATCH 26/43] Add reauth tests --- tests/components/github/test_config_flow.py | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 5476dfa976a20..3d3dade9c5ee1 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -131,3 +131,65 @@ async def test_reauth_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMoc assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "reauth_successful" + + +async def test_reauth_form_cannot_connect(hass: HomeAssistant): + """Test we handle cannot connect error.""" + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIAuthenticationException, + ): + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "invalid_auth"} + + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_REAUTH_INPUT + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_form_cannot_find_repo(hass: HomeAssistant): + """Test we handle cannot connect error.""" + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIAuthenticationException, + ): + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "invalid_auth"} + + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_REAUTH_INPUT + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_find_repo"} From 5dab906664114e4f444e45f8505dd189513a8a06 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 15:32:42 +0100 Subject: [PATCH 27/43] Add tests and json fixture --- tests/components/github/test_config_flow.py | 78 ++++- tests/fixtures/github/repository.json | 362 ++++++++++++++++++++ 2 files changed, 432 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/github/repository.json diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 3d3dade9c5ee1..f2855b222d7bd 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -1,20 +1,47 @@ """Test the GitHub config flow.""" +import json + from aiogithubapi import AIOGitHubAPIAuthenticationException, AIOGitHubAPIException from aiogithubapi.objects.repository import AIOGitHubAPIRepository from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.github.const import CONF_REPOSITORY, DOMAIN +from homeassistant.components.github.const import ( + CONF_CLONES, + CONF_ISSUES_PRS, + CONF_LATEST_COMMIT, + CONF_LATEST_RELEASE, + CONF_REPOSITORY, + CONF_VIEWS, + DOMAIN, +) from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker FIXTURE_REAUTH_INPUT = {CONF_ACCESS_TOKEN: "abc234"} -FIXTURE_USER_INPUT = {CONF_ACCESS_TOKEN: "abc123", CONF_REPOSITORY: "test/repo"} - -UNIQUE_ID = "test/repo" +FIXTURE_USER_INPUT = { + CONF_ACCESS_TOKEN: "abc123", + CONF_REPOSITORY: "octocat/Hello-World", +} +FIXTURE_OPTIONS_DEFAULT = { + CONF_CLONES: False, + CONF_ISSUES_PRS: False, + CONF_LATEST_COMMIT: True, + CONF_LATEST_RELEASE: False, + CONF_VIEWS: False, +} +FIXTURE_OPTIONS_ALL = { + CONF_CLONES: True, + CONF_ISSUES_PRS: True, + CONF_LATEST_COMMIT: True, + CONF_LATEST_RELEASE: True, + CONF_VIEWS: True, +} + +UNIQUE_ID = "octocat/Hello-World" async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): @@ -28,7 +55,9 @@ async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): with patch( "homeassistant.components.github.config_flow.GitHub.get_repo", - return_value=AIOGitHubAPIRepository(aioclient_mock, {"name": "repo"}), + return_value=AIOGitHubAPIRepository( + aioclient_mock, json.loads(load_fixture("github/repository.json")) + ), ), patch( "homeassistant.components.github.async_setup", return_value=True ) as mock_setup, patch( @@ -39,7 +68,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "repo" + assert result2["title"] == "Hello-World" assert result2["data"] == FIXTURE_USER_INPUT await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -121,7 +150,9 @@ async def test_reauth_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMoc with patch( "homeassistant.components.github.config_flow.GitHub.get_repo", - return_value=AIOGitHubAPIRepository(aioclient_mock, {"name": "repo"}), + return_value=AIOGitHubAPIRepository( + aioclient_mock, json.loads(load_fixture("github/repository.json")) + ), ), patch("homeassistant.components.github.async_setup", return_value=True), patch( "homeassistant.components.github.async_setup_entry", return_value=True ): @@ -193,3 +224,34 @@ async def test_reauth_form_cannot_find_repo(hass: HomeAssistant): assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_find_repo"} + + +async def test_options_flow(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test config flow options.""" + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + return_value=AIOGitHubAPIRepository( + aioclient_mock, json.loads(load_fixture("github/repository.json")) + ), + ), patch("homeassistant.components.github.async_setup", return_value=True), patch( + "homeassistant.components.github.async_setup_entry", return_value=True + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=UNIQUE_ID, + data=FIXTURE_USER_INPUT, + options=FIXTURE_OPTIONS_DEFAULT, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + # assert result["step_id"] == "init" + + # result = await hass.config_entries.options.async_configure( + # result["flow_id"], user_input=FIXTURE_OPTIONS_ALL + # ) + + # assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + # assert config_entry.options == FIXTURE_OPTIONS_ALL diff --git a/tests/fixtures/github/repository.json b/tests/fixtures/github/repository.json new file mode 100644 index 0000000000000..faf700c6eb427 --- /dev/null +++ b/tests/fixtures/github/repository.json @@ -0,0 +1,362 @@ +{ + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "http://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "http://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "http://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "http://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "http://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "http://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "http://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "http://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "http://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "http://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "http://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "http://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "http://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "http://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "http://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "http://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "http://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "http://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "http://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "http://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "http://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "http://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "http://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "http://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "http://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "language": null, + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "is_template": true, + "topics": [ + "octocat", + "atom", + "electron", + "api" + ], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "pull": true, + "triage": true, + "push": false, + "maintain": false, + "admin": false + }, + "allow_rebase_merge": true, + "template_repository": null, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + "organization": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "Organization", + "site_admin": false + }, + "parent": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "http://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "http://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "http://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "http://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "http://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "http://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "http://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "http://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "http://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "http://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "http://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "http://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "http://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "http://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "http://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "http://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "http://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "http://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "http://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "http://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "http://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "http://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "http://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "http://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "http://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "language": null, + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "is_template": true, + "topics": [ + "octocat", + "atom", + "electron", + "api" + ], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "allow_rebase_merge": true, + "template_repository": null, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0 + }, + "source": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "http://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "http://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "http://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "http://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "http://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "http://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "http://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "http://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "http://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "http://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "http://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "http://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "http://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "http://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "http://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "http://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "http://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "http://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "http://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "http://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "http://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "http://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "http://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "http://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "http://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "language": null, + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "is_template": true, + "topics": [ + "octocat", + "atom", + "electron", + "api" + ], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "allow_rebase_merge": true, + "template_repository": null, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0 + } +} From 36ea668a8e0941307cdc73f0442cdccf63203f6d Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 15:34:50 +0100 Subject: [PATCH 28/43] Update aiogithubapi to 1.1.1 --- homeassistant/components/github/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index b412133d62475..0465d1a306f0e 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "aiogithubapi==1.0.4" + "aiogithubapi==1.1.1" ], "ssdp": [], "zeroconf": [], diff --git a/requirements_all.txt b/requirements_all.txt index 2f22b62bd7603..8ee331cdb5da1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiofreepybox==0.0.8 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==1.0.4 +aiogithubapi==1.1.1 # homeassistant.components.guardian aioguardian==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1bd4963a3620..92dfbfb3c9c24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioflo==0.4.1 aiofreepybox==0.0.8 # homeassistant.components.github -aiogithubapi==1.0.4 +aiogithubapi==1.1.1 # homeassistant.components.guardian aioguardian==1.0.1 From 25c29d0c25a0ee19bea0f6ebaa62bb1d1bebd49c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 16:55:20 +0100 Subject: [PATCH 29/43] Update data --- homeassistant/components/github/__init__.py | 67 ++++--------------- homeassistant/components/github/sensor.py | 72 +++++++++++---------- 2 files changed, 49 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index a0d076a9c96f8..e0ff9410051c1 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -9,11 +9,16 @@ AIOGitHubAPIException, GitHub, ) +from aiogithubapi.objects.repos.traffic.clones import AIOGitHubAPIReposTrafficClones +from aiogithubapi.objects.repos.traffic.pageviews import ( + AIOGitHubAPIReposTrafficPageviews, +) from aiogithubapi.objects.repository import ( AIOGitHubAPIRepository, AIOGitHubAPIRepositoryIssue, AIOGitHubAPIRepositoryRelease, ) +from aiogithubapi.objects.repository.commit import AIOGitHubAPIRepositoryCommit import async_timeout from homeassistant.config_entries import ConfigEntry @@ -40,45 +45,17 @@ PLATFORMS = ["sensor"] -class GitHubClones: - """Represents a GitHub clones object.""" - - def __init__(self, count: int, count_uniques: int) -> None: - """Initialize a GitHub clones object.""" - self.count = count - self.count_uniques = count_uniques - - -class GitHubLatestCommit: - """Represents a GitHub last commit object.""" - - def __init__(self, sha: int, message: str) -> None: - """Initialize a GitHub last commit object.""" - self.sha = sha - self.sha_short = sha[0:7] - self.message = message.splitlines()[0] - - -class GitHubViews: - """Represents a GitHub views object.""" - - def __init__(self, count: int, count_uniques: int) -> None: - """Initialize a GitHub views object.""" - self.count = count - self.count_uniques = count_uniques - - class GitHubData: """Represents a GitHub data object.""" def __init__( self, repository: AIOGitHubAPIRepository, - latest_commit: GitHubLatestCommit = None, - clones: GitHubClones = None, + latest_commit: AIOGitHubAPIRepositoryCommit = None, + clones: AIOGitHubAPIReposTrafficClones = None, issues: List[AIOGitHubAPIRepositoryIssue] = None, releases: List[AIOGitHubAPIRepositoryRelease] = None, - views: GitHubViews = None, + views: AIOGitHubAPIReposTrafficPageviews = None, ) -> None: """Initialize the GitHub data object.""" self.repository = repository @@ -131,9 +108,7 @@ async def async_update_data() -> GitHubData: entry.data[CONF_REPOSITORY] ) if entry.options.get(CONF_LATEST_COMMIT, True) is True: - latest_commit = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/branches/{repository.default_branch}" - ) + latest_commit: AIOGitHubAPIRepositoryCommit = await repository.get_last_commit() else: latest_commit = None if entry.options.get(CONF_ISSUES_PRS, False) is True: @@ -150,15 +125,11 @@ async def async_update_data() -> GitHubData: releases = None if repository.attributes.get("permissions").get("push") is True: if entry.options.get(CONF_CLONES, False) is True: - clones = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/clones" - ) + clones: AIOGitHubAPIReposTrafficClones = await repository.traffic.get_clones() else: clones = None if entry.options.get(CONF_VIEWS, False) is True: - views = await repository.client.get( - endpoint=f"/repos/{repository.full_name}/traffic/views" - ) + views: AIOGitHubAPIReposTrafficPageviews = await repository.traffic.get_views() else: views = None else: @@ -166,21 +137,7 @@ async def async_update_data() -> GitHubData: views = None return GitHubData( - repository, - GitHubLatestCommit( - latest_commit["commit"]["sha"], - latest_commit["commit"]["commit"]["message"], - ) - if latest_commit is not None - else None, - GitHubClones(clones["count"], clones["uniques"]) - if clones is not None - else None, - issues, - releases, - GitHubViews(views["count"], views["uniques"]) - if views is not None - else None, + repository, latest_commit, clones, issues, releases, views ) except (AIOGitHubAPIAuthenticationException, AIOGitHubAPIException) as err: raise UpdateFailed(f"Error communicating with GitHub: {err}") diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index b8635ba9078f2..dce8e45f5dfba 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -136,7 +136,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, "clones", f"{name} Clones", "mdi:github" ) @@ -150,11 +150,11 @@ async def _github_update(self) -> bool: self._attributes = { ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, - ATTR_UNIQUE: data.clones.count_uniques, + ATTR_UNIQUE: data.clones.uniques, } return True @@ -167,7 +167,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, "forks", f"{name} Forks", "mdi:github" ) @@ -177,12 +177,12 @@ async def _github_update(self) -> bool: await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data - self._state = data.repository.attributes.get("forks") + self._state = data.repository.forks_count self._attributes = { ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, } @@ -197,7 +197,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, @@ -211,16 +211,18 @@ async def _github_update(self) -> bool: await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data - self._state = data.latest_commit.sha_short + # self._state = data.latest_commit.sha self._attributes = { - ATTR_MESSAGE: data.latest_commit.message, - ATTR_SHA: data.latest_commit.sha, + # ATTR_MESSAGE: data.latest_commit.commit.message_short, ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, + # ATTR_SHA: data.latest_commit.sha, + # ATTR_URL: data.latest_commit.html_url, + # ATTR_USER: data.latest_commit.author.login, } return True @@ -233,7 +235,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, @@ -263,8 +265,8 @@ async def _github_update(self) -> bool: ATTR_NUMBER: data.open_issues[0].number, ATTR_OPEN: len(data.open_issues), ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, ATTR_URL: data.open_issues[0].html_url, @@ -281,7 +283,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, @@ -311,8 +313,8 @@ async def _github_update(self) -> bool: ATTR_NUMBER: data.open_pull_requests[0].number, ATTR_OPEN: len(data.open_pull_requests), ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, ATTR_USER: data.open_pull_requests[0].user.login, @@ -328,7 +330,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, @@ -354,8 +356,8 @@ async def _github_update(self) -> bool: ATTR_PRERELEASE: data.releases[0].prerelease, ATTR_RELEASES: len(data.releases), ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, ATTR_URL: f"https://github.com/{data.repository.full_name}/releases/{data.releases[0].tag_name}", @@ -371,7 +373,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, "stargazers", f"{name} Stargazers", "mdi:github" ) @@ -381,12 +383,12 @@ async def _github_update(self) -> bool: await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data - self._state = data.repository.attributes.get("stargazers_count") + self._state = data.repository.stargazers_count self._attributes = { ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, } @@ -401,7 +403,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, "views", f"{name} Views", "mdi:github" ) @@ -415,11 +417,11 @@ async def _github_update(self) -> bool: self._attributes = { ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, - ATTR_UNIQUE: data.views.count_uniques, + ATTR_UNIQUE: data.views.uniques, } return True @@ -432,7 +434,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, repository: AIOGitHubAPIRepository ) -> None: """Initialize the sensor.""" - name = repository.attributes.get("name") + name = repository.name super().__init__( coordinator, repository, "watchers", f"{name} Watchers", "mdi:github" ) @@ -442,12 +444,12 @@ async def _github_update(self) -> bool: await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data - self._state = data.repository.attributes.get("watchers_count") + self._state = data.repository.watchers_count self._attributes = { ATTR_REPO_DESCRIPTION: data.repository.description, - ATTR_REPO_HOMEPAGE: data.repository.attributes.get("homepage"), - ATTR_REPO_NAME: data.repository.attributes.get("name"), + ATTR_REPO_HOMEPAGE: data.repository.homepage, + ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, } From 80498f0fd72b3fbe676c6f1a3c456d81fe978933 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 17:08:48 +0100 Subject: [PATCH 30/43] Update aiogithubapi to 1.1.2 and add sha and message --- homeassistant/components/github/__init__.py | 6 +++--- homeassistant/components/github/manifest.json | 2 +- homeassistant/components/github/sensor.py | 11 ++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index e0ff9410051c1..28bd7bc7a3b8a 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -9,6 +9,7 @@ AIOGitHubAPIException, GitHub, ) +from aiogithubapi.objects.repos.commit import AIOGitHubAPIReposCommit from aiogithubapi.objects.repos.traffic.clones import AIOGitHubAPIReposTrafficClones from aiogithubapi.objects.repos.traffic.pageviews import ( AIOGitHubAPIReposTrafficPageviews, @@ -18,7 +19,6 @@ AIOGitHubAPIRepositoryIssue, AIOGitHubAPIRepositoryRelease, ) -from aiogithubapi.objects.repository.commit import AIOGitHubAPIRepositoryCommit import async_timeout from homeassistant.config_entries import ConfigEntry @@ -51,7 +51,7 @@ class GitHubData: def __init__( self, repository: AIOGitHubAPIRepository, - latest_commit: AIOGitHubAPIRepositoryCommit = None, + latest_commit: AIOGitHubAPIReposCommit = None, clones: AIOGitHubAPIReposTrafficClones = None, issues: List[AIOGitHubAPIRepositoryIssue] = None, releases: List[AIOGitHubAPIRepositoryRelease] = None, @@ -108,7 +108,7 @@ async def async_update_data() -> GitHubData: entry.data[CONF_REPOSITORY] ) if entry.options.get(CONF_LATEST_COMMIT, True) is True: - latest_commit: AIOGitHubAPIRepositoryCommit = await repository.get_last_commit() + latest_commit: AIOGitHubAPIReposCommit = await repository.get_last_commit() else: latest_commit = None if entry.options.get(CONF_ISSUES_PRS, False) is True: diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 0465d1a306f0e..7b6929506d891 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "aiogithubapi==1.1.1" + "aiogithubapi==1.1.2" ], "ssdp": [], "zeroconf": [], diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index dce8e45f5dfba..a4de7dbd3c592 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for GitHub integration.""" import logging +from aiogithubapi.helpers import short_message, short_sha from aiogithubapi.objects.repository import AIOGitHubAPIRepository from homeassistant.const import ATTR_DATE, ATTR_ID, ATTR_NAME @@ -211,18 +212,18 @@ async def _github_update(self) -> bool: await self._coordinator.async_request_refresh() data: GitHubData = self._coordinator.data - # self._state = data.latest_commit.sha + self._state = short_sha(data.latest_commit.sha) self._attributes = { - # ATTR_MESSAGE: data.latest_commit.commit.message_short, + ATTR_MESSAGE: short_message(data.latest_commit.commit.message), ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.homepage, ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, - # ATTR_SHA: data.latest_commit.sha, - # ATTR_URL: data.latest_commit.html_url, - # ATTR_USER: data.latest_commit.author.login, + ATTR_SHA: data.latest_commit.sha, + ATTR_URL: data.latest_commit.html_url, + ATTR_USER: data.latest_commit.author.login, } return True diff --git a/requirements_all.txt b/requirements_all.txt index 8ee331cdb5da1..202b696228e99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiofreepybox==0.0.8 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==1.1.1 +aiogithubapi==1.1.2 # homeassistant.components.guardian aioguardian==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92dfbfb3c9c24..b58ac16a7a93d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioflo==0.4.1 aiofreepybox==0.0.8 # homeassistant.components.github -aiogithubapi==1.1.1 +aiogithubapi==1.1.2 # homeassistant.components.guardian aioguardian==1.0.1 From 6d9c6ad94b9d3f559be3fd8bb919a3cb29c56d8a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 21:45:29 +0100 Subject: [PATCH 31/43] Make push access true for fixture --- tests/fixtures/github/repository.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/github/repository.json b/tests/fixtures/github/repository.json index faf700c6eb427..9baa8822bd6b3 100644 --- a/tests/fixtures/github/repository.json +++ b/tests/fixtures/github/repository.json @@ -347,7 +347,7 @@ "updated_at": "2011-01-26T19:14:43Z", "permissions": { "admin": false, - "push": false, + "push": true, "pull": true }, "allow_rebase_merge": true, From 549fcbea0329054aaad576b3ba2676aab43f69a4 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 22:30:55 +0100 Subject: [PATCH 32/43] Rework reauth logic --- .../components/github/config_flow.py | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index b89ec2febe783..eaec028a31725 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -60,8 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.access_token = None - self.repository = None + self._repository = None async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -87,42 +86,33 @@ async def async_step_reauth(self, user_input): """Handle configuration by re-auth.""" errors = {} - if user_input is None: - user_input = {} - - if user_input.get(CONF_ACCESS_TOKEN) is None and self.access_token is not None: - user_input[CONF_ACCESS_TOKEN] = self.access_token - else: - self.access_token = user_input[CONF_ACCESS_TOKEN] - - if user_input.get(CONF_REPOSITORY) is None and self.repository is not None: - user_input[CONF_REPOSITORY] = self.repository - else: - self.repository = user_input[CONF_REPOSITORY] + if user_input is not None: + if user_input.get(CONF_REPOSITORY) is not None: + self._repository = user_input[CONF_REPOSITORY] + # pylint: disable=no-member + self.context["title_placeholders"] = { + "repository": user_input[CONF_REPOSITORY] + } - if self.context is None: - self.context = {} - # pylint: disable=no-member - self.context["title_placeholders"] = { - "repository": user_input[CONF_REPOSITORY], - } + elif user_input.get(CONF_ACCESS_TOKEN) is not None: + user_input[CONF_REPOSITORY] = self._repository - await self.async_set_unique_id(self.repository) + await self.async_set_unique_id(user_input[CONF_REPOSITORY]) - try: - await validate_input(self.hass, user_input) - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input, - ) - return self.async_abort(reason="reauth_successful") - except CannotConnect: - errors["base"] = "cannot_connect" - except CannotFindRepo: - errors["base"] = "cannot_find_repo" - except InvalidAuth: - errors["base"] = "invalid_auth" + try: + await validate_input(self.hass, user_input) + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, data=user_input, + ) + return self.async_abort(reason="reauth_successful") + except CannotConnect: + errors["base"] = "cannot_connect" + except CannotFindRepo: + errors["base"] = "cannot_find_repo" + except InvalidAuth: + errors["base"] = "invalid_auth" return self.async_show_form( step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors From 8ac5bb4d9a183920a7a963c36c84696de9ac231b Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 23:02:52 +0100 Subject: [PATCH 33/43] Update tests for changes --- .../components/github/config_flow.py | 2 +- tests/components/github/test_config_flow.py | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index eaec028a31725..6dfc4ba9f129c 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -175,7 +175,7 @@ async def async_step_user(self, user_input=None): ): bool, } - return self.async_show_form(step_id="user", data_schema=vol.Schema(schema),) + return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) class CannotConnect(exceptions.HomeAssistantError): diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index f2855b222d7bd..7c84bef51a179 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -146,7 +146,7 @@ async def test_reauth_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMoc assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {} with patch( "homeassistant.components.github.config_flow.GitHub.get_repo", @@ -181,7 +181,7 @@ async def test_reauth_form_cannot_connect(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {} with patch( "homeassistant.components.github.config_flow.GitHub.get_repo", @@ -212,7 +212,7 @@ async def test_reauth_form_cannot_find_repo(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {} with patch( "homeassistant.components.github.config_flow.GitHub.get_repo", @@ -234,24 +234,22 @@ async def test_options_flow(hass: HomeAssistant, aioclient_mock: AiohttpClientMo aioclient_mock, json.loads(load_fixture("github/repository.json")) ), ), patch("homeassistant.components.github.async_setup", return_value=True), patch( - "homeassistant.components.github.async_setup_entry", return_value=True + "homeassistant.components.github.async_setup_entry", return_value=True, ): - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id=UNIQUE_ID, - data=FIXTURE_USER_INPUT, - options=FIXTURE_OPTIONS_DEFAULT, + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT ) - config_entry.add_to_hass(hass) + mock_config.add_to_hass(hass) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result2 = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - # assert result["step_id"] == "init" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + # assert result2["step_id"] == "init" - # result = await hass.config_entries.options.async_configure( - # result["flow_id"], user_input=FIXTURE_OPTIONS_ALL + # result3 = await hass.config_entries.options.async_configure( + # result2["flow_id"], user_input=FIXTURE_OPTIONS_ALL # ) - # assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + # assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # assert config_entry.options == FIXTURE_OPTIONS_ALL From 421fc332ca7e771ecae1fb9bdd042b267d6a139a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 23:24:37 +0100 Subject: [PATCH 34/43] Add reauth invalid auth test --- tests/components/github/test_config_flow.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 7c84bef51a179..db252ad7bd330 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -226,6 +226,37 @@ async def test_reauth_form_cannot_find_repo(hass: HomeAssistant): assert result2["errors"] == {"base": "cannot_find_repo"} +async def test_reauth_form_invalid_auth(hass: HomeAssistant): + """Test we handle cannot connect error.""" + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIAuthenticationException, + ): + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {} + + with patch( + "homeassistant.components.github.config_flow.GitHub.get_repo", + side_effect=AIOGitHubAPIAuthenticationException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + async def test_options_flow(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): """Test config flow options.""" with patch( From ab86e6689c2507a855a3e82c81de43eddf841b32 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 23:26:35 +0100 Subject: [PATCH 35/43] Typo --- tests/components/github/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index db252ad7bd330..92d7b17cedee0 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -250,7 +250,7 @@ async def test_reauth_form_invalid_auth(hass: HomeAssistant): side_effect=AIOGitHubAPIAuthenticationException, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT + result["flow_id"], FIXTURE_REAUTH_INPUT ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM From 4963d0fa126476dcd10372894155839842260614 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 2 Aug 2020 23:33:57 +0100 Subject: [PATCH 36/43] Split identifiers and set as service --- homeassistant/components/github/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 28bd7bc7a3b8a..39b65b570910d 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -252,7 +252,10 @@ def device_info(self) -> Dict[str, Any]: data: GitHubData = self._coordinator.data return { - "identifiers": {(DOMAIN, data.repository.full_name)}, + "entry_type": "service", + "identifiers": { + (DOMAIN, data.repository.owner.login, data.repository.name) + }, "manufacturer": data.repository.attributes.get("owner").get("login"), "name": data.repository.full_name, } From 21763a621ce8f81d9163cda700d8f2bceccc54c6 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 3 Aug 2020 01:00:57 +0100 Subject: [PATCH 37/43] Tests --- tests/components/github/test_config_flow.py | 32 +++++++++------------ 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 92d7b17cedee0..bd3fd0f672358 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -6,12 +6,10 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.github.const import ( - CONF_CLONES, CONF_ISSUES_PRS, CONF_LATEST_COMMIT, CONF_LATEST_RELEASE, CONF_REPOSITORY, - CONF_VIEWS, DOMAIN, ) from homeassistant.const import CONF_ACCESS_TOKEN @@ -27,18 +25,14 @@ CONF_REPOSITORY: "octocat/Hello-World", } FIXTURE_OPTIONS_DEFAULT = { - CONF_CLONES: False, CONF_ISSUES_PRS: False, CONF_LATEST_COMMIT: True, CONF_LATEST_RELEASE: False, - CONF_VIEWS: False, } -FIXTURE_OPTIONS_ALL = { - CONF_CLONES: True, +FIXTURE_OPTIONS_ENABLED = { CONF_ISSUES_PRS: True, CONF_LATEST_COMMIT: True, CONF_LATEST_RELEASE: True, - CONF_VIEWS: True, } UNIQUE_ID = "octocat/Hello-World" @@ -264,23 +258,23 @@ async def test_options_flow(hass: HomeAssistant, aioclient_mock: AiohttpClientMo return_value=AIOGitHubAPIRepository( aioclient_mock, json.loads(load_fixture("github/repository.json")) ), - ), patch("homeassistant.components.github.async_setup", return_value=True), patch( - "homeassistant.components.github.async_setup_entry", return_value=True, ): mock_config = MockConfigEntry( - domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT + domain=DOMAIN, + unique_id=UNIQUE_ID, + data=FIXTURE_USER_INPUT, + options=FIXTURE_OPTIONS_DEFAULT, ) mock_config.add_to_hass(hass) - await hass.async_block_till_done() - result2 = await hass.config_entries.options.async_init(mock_config.entry_id) + result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - # assert result2["step_id"] == "init" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + # assert result["step_id"] == "init" - # result3 = await hass.config_entries.options.async_configure( - # result2["flow_id"], user_input=FIXTURE_OPTIONS_ALL - # ) + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input=FIXTURE_OPTIONS_ENABLED, + ) - # assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - # assert config_entry.options == FIXTURE_OPTIONS_ALL + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == FIXTURE_OPTIONS_ENABLED From 2c826b336decb5f317a5f9f859cda5ea2c0e4666 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 5 Sep 2020 21:29:43 +0100 Subject: [PATCH 38/43] Format --- homeassistant/components/github/__init__.py | 12 +++++++++--- homeassistant/components/github/config_flow.py | 3 ++- tests/components/github/test_config_flow.py | 6 ++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 39b65b570910d..70f4b90788615 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -108,7 +108,9 @@ async def async_update_data() -> GitHubData: entry.data[CONF_REPOSITORY] ) if entry.options.get(CONF_LATEST_COMMIT, True) is True: - latest_commit: AIOGitHubAPIReposCommit = await repository.get_last_commit() + latest_commit: AIOGitHubAPIReposCommit = ( + await repository.get_last_commit() + ) else: latest_commit = None if entry.options.get(CONF_ISSUES_PRS, False) is True: @@ -125,11 +127,15 @@ async def async_update_data() -> GitHubData: releases = None if repository.attributes.get("permissions").get("push") is True: if entry.options.get(CONF_CLONES, False) is True: - clones: AIOGitHubAPIReposTrafficClones = await repository.traffic.get_clones() + clones: AIOGitHubAPIReposTrafficClones = ( + await repository.traffic.get_clones() + ) else: clones = None if entry.options.get(CONF_VIEWS, False) is True: - views: AIOGitHubAPIReposTrafficPageviews = await repository.traffic.get_views() + views: AIOGitHubAPIReposTrafficPageviews = ( + await repository.traffic.get_views() + ) else: views = None else: diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 6dfc4ba9f129c..5e1b35bf379dd 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -104,7 +104,8 @@ async def async_step_reauth(self, user_input): for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: self.hass.config_entries.async_update_entry( - entry, data=user_input, + entry, + data=user_input, ) return self.async_abort(reason="reauth_successful") except CannotConnect: diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index bd3fd0f672358..cc80a15e7167d 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -55,7 +55,8 @@ async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): ), patch( "homeassistant.components.github.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.github.async_setup_entry", return_value=True, + "homeassistant.components.github.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -273,7 +274,8 @@ async def test_options_flow(hass: HomeAssistant, aioclient_mock: AiohttpClientMo # assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( - result["flow_id"], user_input=FIXTURE_OPTIONS_ENABLED, + result["flow_id"], + user_input=FIXTURE_OPTIONS_ENABLED, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From ab8f55cb4495318e1243ac43f34131f2f191aaf0 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 5 Sep 2020 22:33:39 +0100 Subject: [PATCH 39/43] Lint --- homeassistant/components/github/__init__.py | 2 +- homeassistant/components/github/config_flow.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 70f4b90788615..58da7c9c5fef4 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -146,7 +146,7 @@ async def async_update_data() -> GitHubData: repository, latest_commit, clones, issues, releases, views ) except (AIOGitHubAPIAuthenticationException, AIOGitHubAPIException) as err: - raise UpdateFailed(f"Error communicating with GitHub: {err}") + raise UpdateFailed(f"Error communicating with GitHub: {err}") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 5e1b35bf379dd..6057dba8d2e24 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -44,10 +44,10 @@ async def validate_input(hass: core.HomeAssistant, data): ) if repository is None: raise CannotFindRepo - except AIOGitHubAPIAuthenticationException: - raise InvalidAuth - except AIOGitHubAPIException: - raise CannotConnect + except AIOGitHubAPIAuthenticationException as err: + raise InvalidAuth from err + except AIOGitHubAPIException as err: + raise CannotConnect from err return {"title": repository.attributes.get("name")} From e621394b900cf5c955cd7dfc368469ff4e508e3b Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 5 Sep 2020 22:34:57 +0100 Subject: [PATCH 40/43] Remove bad lines from rebase --- .coveragerc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 75b771159faa3..604a11677f837 100644 --- a/.coveragerc +++ b/.coveragerc @@ -312,9 +312,6 @@ omit = homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/geizhals/sensor.py - homeassistant/components/github/sensor.py - homeassistant/components/gios/__init__.py - homeassistant/components/gios/air_quality.py homeassistant/components/github/__init__.py homeassistant/components/github/const.py homeassistant/components/github/sensor.py From 9419154d66335e1e8c722139f82594a85a7293cc Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 5 Sep 2020 23:26:13 +0100 Subject: [PATCH 41/43] Remove unneeded removal --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 604a11677f837..c65cd72d11d3c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -313,7 +313,6 @@ omit = homeassistant/components/geniushub/* homeassistant/components/geizhals/sensor.py homeassistant/components/github/__init__.py - homeassistant/components/github/const.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py From c06d97818b9d30fbd4eb4ba357d4e6b9acd7694d Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 6 Sep 2020 00:12:49 +0100 Subject: [PATCH 42/43] Update to use CoordinatorEntity --- homeassistant/components/github/__init__.py | 37 +--- homeassistant/components/github/sensor.py | 198 ++++++++++---------- 2 files changed, 108 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 58da7c9c5fef4..193ec28ab9985 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -25,8 +25,11 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( CONF_CLONES, @@ -193,14 +196,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -class GitHubEntity(Entity): +class GitHubEntity(CoordinatorEntity): """Defines a GitHub entity.""" def __init__( self, coordinator: DataUpdateCoordinator, unique_id: str, name: str, icon: str ) -> None: """Set up GitHub Entity.""" - self._coordinator = coordinator + super().__init__(coordinator) self._unique_id = unique_id self._name = name self._icon = icon @@ -224,29 +227,7 @@ def icon(self) -> str: @property def available(self) -> bool: """Return True if entity is available.""" - return self._coordinator.last_update_success and self._available - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Update GitHub entity.""" - if await self._github_update(): - self._available = True - else: - self._available = False - - async def _github_update(self) -> bool: - """Update GitHub entity.""" - raise NotImplementedError() + return self.coordinator.last_update_success and self._available class GitHubDeviceEntity(GitHubEntity): @@ -255,7 +236,7 @@ class GitHubDeviceEntity(GitHubEntity): @property def device_info(self) -> Dict[str, Any]: """Return device information about this GitHub instance.""" - data: GitHubData = self._coordinator.data + data: GitHubData = self.coordinator.data return { "entry_type": "service", diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a4de7dbd3c592..55e631b03750a 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -119,16 +119,6 @@ def __init__( super().__init__(coordinator, f"{repository.full_name}_{unique_id}", name, icon) - @property - def state(self) -> str: - """Return the state of the sensor.""" - return self._state - - @property - def device_state_attributes(self) -> object: - """Return the attributes of the sensor.""" - return self._attributes - class ClonesSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -142,14 +132,17 @@ def __init__( coordinator, repository, "clones", f"{name} Clones", "mdi:github" ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - - self._state = data.clones.count + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data + return data.clones.count - self._attributes = { + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + return { ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.homepage, ATTR_REPO_NAME: data.repository.name, @@ -158,8 +151,6 @@ async def _github_update(self) -> bool: ATTR_UNIQUE: data.clones.uniques, } - return True - class ForksSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -173,14 +164,17 @@ def __init__( coordinator, repository, "forks", f"{name} Forks", "mdi:github" ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - - self._state = data.repository.forks_count + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data + return data.repository.forks_count - self._attributes = { + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + return { ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.homepage, ATTR_REPO_NAME: data.repository.name, @@ -188,8 +182,6 @@ async def _github_update(self) -> bool: ATTR_REPO_TOPICS: data.repository.topics, } - return True - class LatestCommitSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -207,14 +199,17 @@ def __init__( "mdi:github", ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - - self._state = short_sha(data.latest_commit.sha) + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data + return short_sha(data.latest_commit.sha) - self._attributes = { + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + return { ATTR_MESSAGE: short_message(data.latest_commit.commit.message), ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.homepage, @@ -226,8 +221,6 @@ async def _github_update(self) -> bool: ATTR_USER: data.latest_commit.author.login, } - return True - class LatestOpenIssueSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -245,21 +238,24 @@ def __init__( "mdi:github", ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data if data.open_issues is None or len(data.open_issues) < 1: - return False - - self._state = data.open_issues[0].title + return None + return data.open_issues[0].title + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + if data.open_issues is None or len(data.open_issues) < 1: + return None labels = [] for label in data.open_issues[0].labels: labels.append(label.get("name")) - - self._attributes = { + return { ATTR_ASSIGNEES: data.open_issues[0].assignees, ATTR_ID: data.open_issues[0].id, ATTR_LABELS: labels, @@ -274,8 +270,6 @@ async def _github_update(self) -> bool: ATTR_USER: data.open_issues[0].user.login, } - return True - class LatestPullRequestSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -293,21 +287,24 @@ def __init__( "mdi:github", ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data if data.open_pull_requests is None or len(data.open_pull_requests) < 1: - return False - - self._state = data.open_pull_requests[0].title + return None + return data.open_pull_requests[0].title + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + if data.open_pull_requests is None or len(data.open_pull_requests) < 1: + return None labels = [] for label in data.open_pull_requests[0].labels: labels.append(label.get("name")) - - self._attributes = { + return { ATTR_ASSIGNEES: data.open_pull_requests[0].assignees, ATTR_ID: data.open_pull_requests[0].id, ATTR_LABELS: labels, @@ -321,8 +318,6 @@ async def _github_update(self) -> bool: ATTR_USER: data.open_pull_requests[0].user.login, } - return True - class LatestReleaseSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -340,17 +335,21 @@ def __init__( "mdi:github", ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data if data.releases is None or len(data.releases) < 1: - return False + return None + return data.releases[0].tag_name - self._state = data.releases[0].tag_name - - self._attributes = { + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + if data.releases is None or len(data.releases) < 1: + return None + return { ATTR_DATE: data.releases[0].published_at, ATTR_DRAFT: data.releases[0].draft, ATTR_NAME: data.releases[0].name, @@ -364,8 +363,6 @@ async def _github_update(self) -> bool: ATTR_URL: f"https://github.com/{data.repository.full_name}/releases/{data.releases[0].tag_name}", } - return True - class StargazersSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -379,14 +376,17 @@ def __init__( coordinator, repository, "stargazers", f"{name} Stargazers", "mdi:github" ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - - self._state = data.repository.stargazers_count + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data + return data.repository.stargazers_count - self._attributes = { + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + return { ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.homepage, ATTR_REPO_NAME: data.repository.name, @@ -394,8 +394,6 @@ async def _github_update(self) -> bool: ATTR_REPO_TOPICS: data.repository.topics, } - return True - class ViewsSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -409,14 +407,17 @@ def __init__( coordinator, repository, "views", f"{name} Views", "mdi:github" ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - - self._state = data.views.count + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data + return data.views.count - self._attributes = { + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + return { ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.homepage, ATTR_REPO_NAME: data.repository.name, @@ -425,8 +426,6 @@ async def _github_update(self) -> bool: ATTR_UNIQUE: data.views.uniques, } - return True - class WatchersSensor(GitHubSensor): """Representation of a Repository Sensor.""" @@ -440,19 +439,20 @@ def __init__( coordinator, repository, "watchers", f"{name} Watchers", "mdi:github" ) - async def _github_update(self) -> bool: - """Fetch new state data for the sensor.""" - await self._coordinator.async_request_refresh() - data: GitHubData = self._coordinator.data - - self._state = data.repository.watchers_count + @property + def state(self) -> str: + """Return the state of the sensor.""" + data: GitHubData = self.coordinator.data + return data.repository.watchers_count - self._attributes = { + @property + def device_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + data: GitHubData = self.coordinator.data + return { ATTR_REPO_DESCRIPTION: data.repository.description, ATTR_REPO_HOMEPAGE: data.repository.homepage, ATTR_REPO_NAME: data.repository.name, ATTR_REPO_PATH: data.repository.full_name, ATTR_REPO_TOPICS: data.repository.topics, } - - return True From 31712fe0e31fb82ec097a3547541532015f25037 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 11 Oct 2020 14:20:16 +0100 Subject: [PATCH 43/43] Update aiogithubapi to v2.0.0 --- homeassistant/components/github/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 7b6929506d891..8555eb9dce04d 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "aiogithubapi==1.1.2" + "aiogithubapi==2.0.0" ], "ssdp": [], "zeroconf": [], diff --git a/requirements_all.txt b/requirements_all.txt index 202b696228e99..6fa8fdec0d999 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiofreepybox==0.0.8 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==1.1.2 +aiogithubapi==2.0.0 # homeassistant.components.guardian aioguardian==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b58ac16a7a93d..4db5ea10b420e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioflo==0.4.1 aiofreepybox==0.0.8 # homeassistant.components.github -aiogithubapi==1.1.2 +aiogithubapi==2.0.0 # homeassistant.components.guardian aioguardian==1.0.1