Move google_sheets services to separate module (#146160)

* Move google_sheets services to separate module

* Move to async_setup

* Do not remove the services

* hassfest

* Rename
This commit is contained in:
epenet 2025-06-05 15:07:15 +02:00 committed by GitHub
parent 4d3443dbf5
commit 38b8d0b018
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 104 additions and 103 deletions

View File

@ -2,48 +2,33 @@
from __future__ import annotations
from datetime import datetime
import aiohttp
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.exceptions import APIError
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.helpers.typing import ConfigType
from .const import DEFAULT_ACCESS, DOMAIN
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type GoogleSheetsConfigEntry = ConfigEntry[OAuth2Session]
DATA = "data"
DATA_CONFIG_ENTRY = "config_entry"
WORKSHEET = "worksheet"
SERVICE_APPEND_SHEET = "append_sheet"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Activate the Google Sheets component."""
SHEET_SERVICE_SCHEMA = vol.All(
{
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
vol.Optional(WORKSHEET): cv.string,
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
},
)
async_setup_services(hass)
return True
async def async_setup_entry(
@ -67,8 +52,6 @@ async def async_setup_entry(
raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
entry.runtime_data = session
await async_setup_service(hass)
return True
@ -81,55 +64,4 @@ async def async_unload_entry(
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
) -> bool:
"""Unload a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
for service_name in hass.services.async_services_for_domain(DOMAIN):
hass.services.async_remove(DOMAIN, service_name)
return True
async def async_setup_service(hass: HomeAssistant) -> None:
"""Add the services for Google Sheets."""
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
"""Run append in the executor."""
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
try:
sheet = service.open_by_key(entry.unique_id)
except RefreshError:
entry.async_start_reauth(hass)
raise
except APIError as ex:
raise HomeAssistantError("Failed to write data") from ex
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
now = str(datetime.now())
rows = []
for d in call.data[DATA]:
row_data = {"created": now} | d
row = [row_data.get(column, "") for column in columns]
for key, value in row_data.items():
if key not in columns:
columns.append(key)
worksheet.update_cell(1, len(columns), key)
row.append(value)
rows.append(row)
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
async def append_to_sheet(call: ServiceCall) -> None:
"""Append new line of data to a Google Sheets document."""
entry: GoogleSheetsConfigEntry | None = hass.config_entries.async_get_entry(
call.data[DATA_CONFIG_ENTRY]
)
if not entry or not hasattr(entry, "runtime_data"):
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
await entry.runtime_data.async_ensure_token_valid()
await hass.async_add_executor_job(_append_to_sheet, call, entry)
hass.services.async_register(
DOMAIN,
SERVICE_APPEND_SHEET,
append_to_sheet,
schema=SHEET_SERVICE_SCHEMA,
)

View File

@ -0,0 +1,87 @@
"""Support for Google Sheets."""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.exceptions import APIError
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import ConfigEntrySelector
from .const import DOMAIN
if TYPE_CHECKING:
from . import GoogleSheetsConfigEntry
DATA = "data"
DATA_CONFIG_ENTRY = "config_entry"
WORKSHEET = "worksheet"
SERVICE_APPEND_SHEET = "append_sheet"
SHEET_SERVICE_SCHEMA = vol.All(
{
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
vol.Optional(WORKSHEET): cv.string,
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
},
)
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
"""Run append in the executor."""
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
try:
sheet = service.open_by_key(entry.unique_id)
except RefreshError:
entry.async_start_reauth(call.hass)
raise
except APIError as ex:
raise HomeAssistantError("Failed to write data") from ex
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
now = str(datetime.now())
rows = []
for d in call.data[DATA]:
row_data = {"created": now} | d
row = [row_data.get(column, "") for column in columns]
for key, value in row_data.items():
if key not in columns:
columns.append(key)
worksheet.update_cell(1, len(columns), key)
row.append(value)
rows.append(row)
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
async def _async_append_to_sheet(call: ServiceCall) -> None:
"""Append new line of data to a Google Sheets document."""
entry: GoogleSheetsConfigEntry | None = call.hass.config_entries.async_get_entry(
call.data[DATA_CONFIG_ENTRY]
)
if not entry or not hasattr(entry, "runtime_data"):
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
await entry.runtime_data.async_ensure_token_valid()
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
def async_setup_services(hass: HomeAssistant) -> None:
"""Add the services for Google Sheets."""
hass.services.async_register(
DOMAIN,
SERVICE_APPEND_SHEET,
_async_append_to_sheet,
schema=SHEET_SERVICE_SCHEMA,
)

View File

@ -14,10 +14,10 @@ from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.components.google_sheets import DOMAIN
from homeassistant.components.google_sheets.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@ -95,7 +95,6 @@ async def test_setup_success(
assert not hass.data.get(DOMAIN)
assert entries[0].state is ConfigEntryState.NOT_LOADED
assert not hass.services.async_services().get(DOMAIN, {})
@pytest.mark.parametrize(
@ -200,7 +199,7 @@ async def test_append_sheet(
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
with patch("homeassistant.components.google_sheets.Client") as mock_client:
with patch("homeassistant.components.google_sheets.services.Client") as mock_client:
await hass.services.async_call(
DOMAIN,
"append_sheet",
@ -226,7 +225,7 @@ async def test_append_sheet_multiple_rows(
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
with patch("homeassistant.components.google_sheets.Client") as mock_client:
with patch("homeassistant.components.google_sheets.services.Client") as mock_client:
await hass.services.async_call(
DOMAIN,
"append_sheet",
@ -258,7 +257,7 @@ async def test_append_sheet_api_error(
with (
pytest.raises(HomeAssistantError),
patch(
"homeassistant.components.google_sheets.Client.request",
"homeassistant.components.google_sheets.services.Client.request",
side_effect=APIError(response),
),
):
@ -331,20 +330,3 @@ async def test_append_sheet_invalid_config_entry(
},
blocking=True,
)
# Unloading the other config entry will de-register the service
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED
with pytest.raises(ServiceNotFound):
await hass.services.async_call(
DOMAIN,
"append_sheet",
{
"config_entry": config_entry.entry_id,
"worksheet": "Sheet1",
"data": {"foo": "bar"},
},
blocking=True,
)