From c2902124ad5d6d94ec4d91f927ceb8c7596a8eec Mon Sep 17 00:00:00 2001 From: andgineer Date: Wed, 27 Sep 2023 06:55:47 +0200 Subject: [PATCH] https://github.com/andgineer/docker-amazon-dash-button-hack/issues/25 models --- requirements.dev.txt | 21 ++++- requirements.in | 1 + requirements.txt | 14 +++- src/action.py | 2 +- src/models.py | 93 ++++++++++++++++++++++ tests/conftest.py | 21 ++++- tests/resources/settings.json | 142 ++++++++++++++++++++++++++++++++++ tests/test_action.py | 47 +++++++---- 8 files changed, 315 insertions(+), 26 deletions(-) create mode 100644 src/models.py create mode 100644 tests/resources/settings.json diff --git a/requirements.dev.txt b/requirements.dev.txt index dc8d74c..8755dcd 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -4,7 +4,11 @@ # # pip-compile requirements.dev.in # -astroid==2.15.6 +annotated-types==0.5.0 + # via + # -r requirements.txt + # pydantic +astroid==2.15.7 # via pylint bracex==2.4 # via wcmatch @@ -43,11 +47,11 @@ freezegun==1.2.2 # via -r requirements.dev.in ghp-import==2.1.0 # via mkdocs -google-api-core==2.11.1 +google-api-core==2.12.0 # via # -r requirements.txt # google-api-python-client -google-api-python-client==2.100.0 +google-api-python-client==2.101.0 # via -r requirements.txt google-auth==2.23.0 # via @@ -149,9 +153,15 @@ pycparser==2.21 # via # -r requirements.txt # cffi +pydantic==2.4.0 + # via -r requirements.txt +pydantic-core==2.10.0 + # via + # -r requirements.txt + # pydantic pydocstyle==6.3.0 # via -r requirements.dev.in -pylint==2.17.5 +pylint==2.17.6 # via -r requirements.dev.in pyparsing==3.1.1 # via @@ -205,7 +215,10 @@ typer==0.9.0 # via lazydocs typing-extensions==4.8.0 # via + # -r requirements.txt # mypy + # pydantic + # pydantic-core # typer uritemplate==4.1.1 # via diff --git a/requirements.in b/requirements.in index b24421b..b0d8eec 100644 --- a/requirements.in +++ b/requirements.in @@ -6,3 +6,4 @@ google-api-python-client requests cffi httplib2 +pydantic diff --git a/requirements.txt b/requirements.txt index 98bc6e1..5e6d6c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,8 @@ # # pip-compile requirements.in # +annotated-types==0.5.0 + # via pydantic cachetools==5.3.1 # via google-auth certifi==2023.7.22 @@ -12,9 +14,9 @@ cffi==1.15.1 # via -r requirements.in charset-normalizer==3.2.0 # via requests -google-api-core==2.11.1 +google-api-core==2.12.0 # via google-api-python-client -google-api-python-client==2.100.0 +google-api-python-client==2.101.0 # via -r requirements.in google-auth==2.23.0 # via @@ -50,6 +52,10 @@ pyasn1-modules==0.3.0 # oauth2client pycparser==2.21 # via cffi +pydantic==2.4.0 + # via -r requirements.in +pydantic-core==2.10.0 + # via pydantic pyparsing==3.1.1 # via httplib2 python-dateutil==2.8.2 @@ -68,6 +74,10 @@ six==1.16.0 # via # oauth2client # python-dateutil +typing-extensions==4.8.0 + # via + # pydantic + # pydantic-core uritemplate==4.1.1 # via google-api-python-client urllib3==1.26.16 diff --git a/src/action.py b/src/action.py index 8147f1d..83f101c 100644 --- a/src/action.py +++ b/src/action.py @@ -23,7 +23,7 @@ def __init__(self, settings: Dict[str, Any]) -> None: self.actions: Dict[str, Any] = settings["actions"] def set_summary_by_time(self, button_actions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Set even summary according now(). + """Set event summary according now(). If summary is a list like diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..cc7d157 --- /dev/null +++ b/src/models.py @@ -0,0 +1,93 @@ +from typing import Dict, List, Optional, Union + +from pydantic import BaseModel + + +class TimeSummary(BaseModel): # type: ignore + """Summary for a time interval.""" + + summary: str + before: Optional[str] = None + image: str + + +class DashBoardAbsent(BaseModel): # type: ignore + """Absent images.""" + + summary: str + image_grid: str + image_plot: str + + +class DashboardItem(BaseModel): # type: ignore + """Dashboard item.""" + + summary: str + empty_image: str + absent: List[DashBoardAbsent] + + +class SheetAction(BaseModel): # type: ignore + """Action for a google sheet.""" + + type: str + name: str + press_sheet: str + event_sheet: str + restart: int + autoclose: int + default: int + + +class CalendarAction(BaseModel): # type: ignore + """Action for a google calendar.""" + + type: str + calendar_id: str + dashboard: Optional[str] = None + restart: int + autoclose: int + default: int + + +class IftttAction(BaseModel): # type: ignore + """Action for a IFTTT.""" + + type: str + summary: str + value1: Optional[str] = "" + value2: Optional[str] = "" + value3: Optional[str] = "" + + +class OpenhabAction(BaseModel): # type: ignore + """Action for a OpenHab.""" "" + + type: str + path: str + item: str + command: str + + +ActionItem = Union[SheetAction, CalendarAction, IftttAction, OpenhabAction] + + +class ButtonActions(BaseModel): # type: ignore + """Actions for a button.""" + + summary: Union[str, List[TimeSummary]] + actions: List[ActionItem] + + +class Settings(BaseModel): # type: ignore + """Settings for the application.""" "" + + latitude: str + longitude: str + credentials_file_name: str + ifttt_key_file_name: str + openweathermap_key_file_name: str + images_folder: str + bounce_delay: int + dashboards: Dict[str, DashboardItem] + actions: Dict[str, ButtonActions] diff --git a/tests/conftest.py b/tests/conftest.py index c5369ab..e516b57 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +import json +from typing import Any, Dict from unittest.mock import Mock import sys import pytest @@ -17,8 +19,23 @@ # We need to import amazon_dash after mocking scapy from amazon_dash import AmazonDash +from models import Settings, ButtonActions @pytest.fixture(scope="function") -def dash(): - return AmazonDash() \ No newline at end of file +def dash() -> AmazonDash: + return AmazonDash() + + +@pytest.fixture +def settings() -> Dict[str, Any]: + """Read settings from tests/resources/settings.json.""" + return load_settings().model_dump(exclude_unset=True) + + +def load_settings() -> Settings: + print("*" * 10, "load_settings") + with open("tests/resources/settings.json", "r", encoding="utf-8") as settings_file: + settings = Settings(**json.loads(settings_file.read())) + return settings + diff --git a/tests/resources/settings.json b/tests/resources/settings.json new file mode 100644 index 0000000..161e8e6 --- /dev/null +++ b/tests/resources/settings.json @@ -0,0 +1,142 @@ +{ + "latitude": "60.002228", + "longitude": "30.296947", + "credentials_file_name": "../amazon-dash-private/amazon-dash-hack.json", + "ifttt_key_file_name": "../amazon-dash-private/ifttt-key.json", + "openweathermap_key_file_name": "../amazon-dash-private/openweathermap-key.json", + "images_folder": "../amazon-dash-private/images/", + "bounce_delay": 5, + "dashboards": { + "anna_work_out": { + "summary": "Anna work-out", + "empty_image": "old-woman.png", + "absent": [ + { + "summary": "Sick", + "image_grid": "absent_ill_grid.png", + "image_plot": "absent_ill_plot.png" + }, + { + "summary": "Vacation", + "image_grid": "absent_vacation_grid.png", + "image_plot": "absent_vacation_plot.png" + } + ] + }, + "anna_english": { + "summary": "Anna English", + "empty_image": "old-woman.png", + "absent": [ + { + "summary": "Sick", + "image_grid": "absent_ill_grid.png", + "image_plot": "absent_ill_plot.png" + }, + { + "summary": "Vacation", + "image_grid": "absent_vacation_grid.png", + "image_plot": "absent_vacation_plot.png" + } + ] + } + }, + "actions": { + "white": { + "summary": [ + {"summary": "Morning work-out", "before": "12:00:00", "image": "morning.png"}, + {"summary": "Physiotherapy", "image": "evening2.png"} + ], + "actions": [ + { + "type": "sheet", + "name": "amazon_dash", + "press_sheet": "press", + "event_sheet": "event", + "restart": 15, + "autoclose": 10800, + "default": 900 + }, + { + "type": "calendar", + "calendar_id": "eo2n7ip8p1tm6dgseh3e7p19no@group.calendar.google.com", + "dashboard": "anna_work_out", + "restart": 15, + "autoclose": 10800, + "default": 900 + }, + { + "type": "ifttt", + "summary": "{button}_amazon_dash", + "value1": "", + "value2": "", + "value3": "" + } + ] + }, + "violet": { + "summary": [ + {"summary": "English", "image": "evening2.png"} + ], + "actions": [ + { + "type": "sheet", + "name": "amazon_dash", + "press_sheet": "press", + "event_sheet": "event", + "restart": 15, + "autoclose": 10800, + "default": 900 + }, + { + "type": "calendar", + "calendar_id": "eo2n7ip8p1tm6dgseh3e7p19no@group.calendar.google.com", + "dashboard": "anna_english", + "restart": 15, + "autoclose": 10800, + "default": 900 + }, + { + "type": "ifttt", + "summary": "{button}_amazon_dash", + "value1": "", + "value2": "", + "value3": "" + } + ] + }, + "__DEFAULT__": { + "summary": "{button}", + "actions": [ + { + "type": "sheet", + "name": "amazon_dash", + "press_sheet": "press", + "event_sheet": "event", + "restart": 60, + "autoclose": 10800, + "default": 900 + }, + { + "type": "calendar", + "calendar_id": "eo2n7ip8p1tm6dgseh3e7p19no@group.calendar.google.com", + "restart": 15, + "autoclose": 10800, + "default": 900 + }, + { + "type": "ifttt", + "summary": "{button}_amazon_dash", + "value1": "", + "value2": "", + "value3": "" + }, + { + "type": "openhab", + "path": "http://localhost:8080", + "item": "{button}_amazon_dash", + "command": "ON;OFF" + } + ] + } + } +} diff --git a/tests/test_action.py b/tests/test_action.py index bc63e37..9cc19be 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -6,11 +6,8 @@ @patch("action.datetime") -def test_action(fake_datetime, dash): +def test_action(fake_datetime, settings): fake_datetime.now = Mock(return_value=datetime.strptime("19:00:00", "%H:%M:%S")) - settings = dash.load_settings( - "." - ) # we run tests from outside src folder so we have to change path to settings file act = Action(settings) act.ifttt_action = Mock() act.calendar_action = Mock() @@ -54,16 +51,7 @@ def test_action(fake_datetime, dash): @pytest.fixture -def action_instance(): - settings = { - "actions": { - "test_button": { - "actions": [ - {"type": "sheet", "name": "test", "press_sheet": "sheet1", "event_sheet": "sheet2", "summary": "test_summary"}, - ] - } - } - } +def action_instance(settings): return Action(settings) @@ -88,15 +76,40 @@ def test_set_summary_by_time(action_instance): def test_preprocess_actions(action_instance): - result = action_instance.preprocess_actions("test_button", action_instance.actions["test_button"]) + result = action_instance.preprocess_actions("violet", action_instance.actions["violet"]) expected_result = [ - {"type": "sheet", "name": "test", "press_sheet": "sheet1", "event_sheet": "sheet2", "summary": "test_summary"} + { + "type": "sheet", + "name": "amazon_dash", + "press_sheet": "press", + "event_sheet": "event", + "restart": 15, + "autoclose": 10800, + "default": 900, + 'summary': [{'image': 'evening2.png', 'summary': 'English'}], + }, + { + "type": "calendar", + "calendar_id": "eo2n7ip8p1tm6dgseh3e7p19no@group.calendar.google.com", + "dashboard": "anna_english", + "restart": 15, + "autoclose": 10800, + "default": 900, + 'summary': [{'image': 'evening2.png', 'summary': 'English'}], + }, + { + "type": "ifttt", + "summary": "violet_amazon_dash", + "value1": "", + "value2": "", + "value3": "" + } ] assert result == expected_result @patch.object(Action, "sheet_action") -def test_action(mock_sheet_action, action_instance): +def test_mocked_sheet_action(mock_sheet_action, action_instance): action_instance.action("test_button", dry_run=False) mock_sheet_action.assert_called()