mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 13:47:35 +00:00
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:
parent
4d3443dbf5
commit
38b8d0b018
@ -2,48 +2,33 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import aiohttp
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
from homeassistant.const import CONF_TOKEN
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
ConfigEntryAuthFailed,
|
|
||||||
ConfigEntryNotReady,
|
|
||||||
HomeAssistantError,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||||
OAuth2Session,
|
OAuth2Session,
|
||||||
async_get_config_entry_implementation,
|
async_get_config_entry_implementation,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DEFAULT_ACCESS, DOMAIN
|
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]
|
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(
|
async_setup_services(hass)
|
||||||
{
|
|
||||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
return True
|
||||||
vol.Optional(WORKSHEET): cv.string,
|
|
||||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -67,8 +52,6 @@ async def async_setup_entry(
|
|||||||
raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
|
raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
|
||||||
entry.runtime_data = session
|
entry.runtime_data = session
|
||||||
|
|
||||||
await async_setup_service(hass)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -81,55 +64,4 @@ async def async_unload_entry(
|
|||||||
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
|
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""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
|
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,
|
|
||||||
)
|
|
||||||
|
87
homeassistant/components/google_sheets/services.py
Normal file
87
homeassistant/components/google_sheets/services.py
Normal 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,
|
||||||
|
)
|
@ -14,10 +14,10 @@ from homeassistant.components.application_credentials import (
|
|||||||
ClientCredential,
|
ClientCredential,
|
||||||
async_import_client_credential,
|
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.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
@ -95,7 +95,6 @@ async def test_setup_success(
|
|||||||
|
|
||||||
assert not hass.data.get(DOMAIN)
|
assert not hass.data.get(DOMAIN)
|
||||||
assert entries[0].state is ConfigEntryState.NOT_LOADED
|
assert entries[0].state is ConfigEntryState.NOT_LOADED
|
||||||
assert not hass.services.async_services().get(DOMAIN, {})
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -200,7 +199,7 @@ async def test_append_sheet(
|
|||||||
assert len(entries) == 1
|
assert len(entries) == 1
|
||||||
assert entries[0].state is ConfigEntryState.LOADED
|
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(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"append_sheet",
|
"append_sheet",
|
||||||
@ -226,7 +225,7 @@ async def test_append_sheet_multiple_rows(
|
|||||||
assert len(entries) == 1
|
assert len(entries) == 1
|
||||||
assert entries[0].state is ConfigEntryState.LOADED
|
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(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"append_sheet",
|
"append_sheet",
|
||||||
@ -258,7 +257,7 @@ async def test_append_sheet_api_error(
|
|||||||
with (
|
with (
|
||||||
pytest.raises(HomeAssistantError),
|
pytest.raises(HomeAssistantError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.google_sheets.Client.request",
|
"homeassistant.components.google_sheets.services.Client.request",
|
||||||
side_effect=APIError(response),
|
side_effect=APIError(response),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
@ -331,20 +330,3 @@ async def test_append_sheet_invalid_config_entry(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
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,
|
|
||||||
)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user