diff --git a/cookbook/apps.py b/cookbook/apps.py index 63b8296e8..c3fa140a5 100644 --- a/cookbook/apps.py +++ b/cookbook/apps.py @@ -16,15 +16,11 @@ class CookbookConfig(AppConfig): 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 @@ class CookbookConfig(AppConfig): # 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 487aeca0d..4c99281a4 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 904afd078..ba2b4a227 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -3,7 +3,7 @@ from logging import Logger 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 @@ -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 @@ class HomeAssistant(Connector): 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, @@ -50,8 +52,8 @@ class HomeAssistant(Connector): 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)=} {data=}") + 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: @@ -62,14 +64,14 @@ class HomeAssistant(Connector): 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, @@ -78,9 +80,9 @@ class HomeAssistant(Connector): 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