From 8c8834e6aa0a166f97bbc14e7e9d066705684828 Mon Sep 17 00:00:00 2001 From: Mikhail Epifanov Date: Fri, 13 Sep 2024 22:36:05 +0200 Subject: [PATCH 1/2] dont log err.headers which can contain sensitive info --- cookbook/connectors/homeassistant.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cookbook/connectors/homeassistant.py b/cookbook/connectors/homeassistant.py index e2bdcc22db..3268c4cb1d 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -3,7 +3,7 @@ from typing import Dict, Tuple from urllib.parse import urljoin -from aiohttp import ClientError, request +from aiohttp import request, ClientResponseError from cookbook.connectors.connector import Connector from cookbook.models import ShoppingListEntry, ConnectorConfig, Space @@ -48,8 +48,8 @@ async def on_shopping_list_entry_created(self, space: Space, shopping_list_entry try: await self.homeassistant_api_call("POST", "services/todo/add_item", data) - except ClientError as err: - self._logger.warning(f"received an exception from the api: {err=}, {type(err)=}") + except ClientResponseError as err: + self._logger.warning(f"received an exception from the api: {err.request_info.url=}, {err.request_info.method=}, {err.status=}, {err.message=}, {type(err)=}") async def on_shopping_list_entry_updated(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None: if not self._config.on_shopping_list_entry_updated_enabled: @@ -76,9 +76,9 @@ async def on_shopping_list_entry_deleted(self, space: Space, shopping_list_entry try: await self.homeassistant_api_call("POST", "services/todo/remove_item", data) - except ClientError as err: + except ClientResponseError as err: # This error will always trigger if the item is not present/found - self._logger.debug(f"received an exception from the api: {err=}, {type(err)=}") + self._logger.debug(f"received an exception from the api: {err.request_info.url=}, {err.request_info.method=}, {err.status=}, {err.message=}, {type(err)=}") async def close(self) -> None: pass From e6eacc48d6b2560bf9651936c8e763e26cb5f908 Mon Sep 17 00:00:00 2001 From: Mikhail Epifanov Date: Tue, 17 Sep 2024 22:25:08 +0200 Subject: [PATCH 2/2] update code so it also works in DEBUG=false --- cookbook/apps.py | 16 ++++++---------- cookbook/connectors/connector_manager.py | 12 +++++++++++- cookbook/connectors/homeassistant.py | 8 +++++--- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/cookbook/apps.py b/cookbook/apps.py index 63b8296e83..c3fa140a58 100644 --- a/cookbook/apps.py +++ b/cookbook/apps.py @@ -16,15 +16,11 @@ def ready(self): import cookbook.signals # noqa if not settings.DISABLE_EXTERNAL_CONNECTORS: - try: - from cookbook.connectors.connector_manager import ConnectorManager # Needs to be here to prevent loading race condition of oauth2 modules in models.py - handler = ConnectorManager() - post_save.connect(handler, dispatch_uid="connector_manager") - post_delete.connect(handler, dispatch_uid="connector_manager") - except Exception as e: - traceback.print_exc() - print('Failed to initialize connectors') - pass + from cookbook.connectors.connector_manager import ConnectorManager # Needs to be here to prevent loading race condition of oauth2 modules in models.py + handler = ConnectorManager() + post_save.connect(handler, dispatch_uid="post_save-connector_manager") + post_delete.connect(handler, dispatch_uid="post_delete-connector_manager") + # if not settings.DISABLE_TREE_FIX_STARTUP: # # when starting up run fix_tree to: # # a) make sure that nodes are sorted when switching between sort modes @@ -45,4 +41,4 @@ def ready(self): # except Exception: # if DEBUG: # traceback.print_exc() - # pass # dont break startup just because fix could not run, need to investigate cases when this happens + # pass # dont break startup just because fix could not run, need to investigate cases when this happens \ No newline at end of file diff --git a/cookbook/connectors/connector_manager.py b/cookbook/connectors/connector_manager.py index 487aeca0d2..4c99281a42 100644 --- a/cookbook/connectors/connector_manager.py +++ b/cookbook/connectors/connector_manager.py @@ -31,6 +31,15 @@ class Work: actionType: ActionType +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + # The way ConnectionManager works is as follows: # 1. On init, it starts a worker & creates a queue for 'Work' # 2. Then any time its called, it verifies the type of action (create/update/delete) and if the item is of interest, pushes the Work (non-blocking) to the queue. @@ -39,7 +48,8 @@ class Work: # 3.2 If work is of type REGISTERED_CLASSES, it asynchronously fires of all connectors and wait for them to finish (runtime should depend on the 'slowest' connector) # 4. Work is marked as consumed, and next entry of the queue is consumed. # Each 'Work' is processed in sequential by the worker, so the throughput is about [workers * the slowest connector] -class ConnectorManager: +# The Singleton class is used for ConnectorManager to have a self-reference and so Python does not garbage collect it +class ConnectorManager(metaclass=Singleton): _logger: Logger _queue: queue.Queue _listening_to_classes = REGISTERED_CLASSES | ConnectorConfig diff --git a/cookbook/connectors/homeassistant.py b/cookbook/connectors/homeassistant.py index 3268c4cb1d..75d283d5e9 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -13,6 +13,8 @@ class HomeAssistant(Connector): _config: ConnectorConfig _logger: Logger + _required_foreign_keys = ("food", "unit", "created_by") + def __init__(self, config: ConnectorConfig): if not config.token or not config.url or not config.todo_entity: raise ValueError("config for HomeAssistantConnector in incomplete") @@ -38,7 +40,7 @@ async def on_shopping_list_entry_created(self, space: Space, shopping_list_entry item, description = _format_shopping_list_entry(shopping_list_entry) - self._logger.debug(f"adding {item=}") + self._logger.debug(f"adding {item=} with {description=} to {self._config.todo_entity}") data = { "entity_id": self._config.todo_entity, @@ -60,14 +62,14 @@ async def on_shopping_list_entry_deleted(self, space: Space, shopping_list_entry if not self._config.on_shopping_list_entry_deleted_enabled: return - if not hasattr(shopping_list_entry._state.fields_cache, "food"): + if not all(k in shopping_list_entry._state.fields_cache for k in self._required_foreign_keys): # Sometimes the food foreign key is not loaded, and we cant load it from an async process self._logger.debug("required property was not present in ShoppingListEntry") return item, _ = _format_shopping_list_entry(shopping_list_entry) - self._logger.debug(f"removing {item=}") + self._logger.debug(f"removing {item=} from {self._config.todo_entity}") data = { "entity_id": self._config.todo_entity,