From 3491fef81eca80e270f769543c1e14398121a204 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 21 Jun 2022 03:20:27 +0000 Subject: [PATCH 01/40] Added testing infrastructure and test suite job --- .github/workflows/code_rules.yml | 39 ----------------- .github/workflows/test_suite.yml | 70 +++++++++++++++++++++++++++++++ scripts/make_test_server | 24 +++++++++++ tests/homeassistant_2022.6.6.zip | Bin 0 -> 33436 bytes tests/server_config.yaml | 26 ++++++++++++ tests/test_endpoints.py | 12 ++++++ 6 files changed, 132 insertions(+), 39 deletions(-) delete mode 100644 .github/workflows/code_rules.yml create mode 100644 .github/workflows/test_suite.yml create mode 100644 scripts/make_test_server create mode 100644 tests/homeassistant_2022.6.6.zip create mode 100644 tests/server_config.yaml create mode 100644 tests/test_endpoints.py diff --git a/.github/workflows/code_rules.yml b/.github/workflows/code_rules.yml deleted file mode 100644 index 198ef1e0..00000000 --- a/.github/workflows/code_rules.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Code Standards - -on: - push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) - paths: - - "**.py" - pull_request: - branches: - - master - - dev - paths: - - "**.py" - workflow_dispatch: - -jobs: - code_styling: - runs-on: ubuntu-latest - steps: - - name: Setup Python - uses: actions/setup-python@v2 - - name: Checkout - uses: actions/checkout@v2 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Install Dependencies - run: | - pip install poetry - poetry config virtualenvs.create false - poetry install - - name: Run Black - run: black homeassistant_api --check - - name: Run iSort - run: isort homeassistant_api --check-only - - name: Run Flake8 - run: flake8 homeassistant_api - - name: Run MyPy - run: mypy homeassistant_api --show-error-codes - - name: Run PyLint - run: pylint homeassistant_api diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml new file mode 100644 index 00000000..2fd1494d --- /dev/null +++ b/.github/workflows/test_suite.yml @@ -0,0 +1,70 @@ +name: Code Standards + +on: + push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) + paths: + - "**.py" + pull_request: + branches: + - master + - dev + paths: + - "**.py" + workflow_dispatch: + +jobs: + code_styling: + runs-on: ubuntu-latest + steps: + - name: Setup Python + uses: actions/setup-python@v2 + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Install Dependencies + run: | + pip install poetry + poetry config virtualenvs.create false + poetry install + - name: Run Black + run: black homeassistant_api --check + - name: Run iSort + run: isort homeassistant_api --check-only + - name: Run Flake8 + run: flake8 homeassistant_api + - name: Run MyPy + run: mypy homeassistant_api --show-error-codes + - name: Run PyLint + run: pylint homeassistant_api + code_functionality: + runs-on: ubuntu-latest + steps: + - name: Setup Python + uses: actions/setup-python@v2 + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Install Dependencies + run: | + pip install poetry homeassistant + poetry config virtualenvs.create false + poetry install --no-dev --no-root + sudo apt-get -qq install -y python3 \ + python3-dev python3-venv python3-pip \ + libffi-dev libssl-dev libjpeg-dev zlib1g-dev \ + autoconf build-essential libopenjp2-7 libtiff5 \ + libturbojpeg0-dev tzdata + - name: Start Server + run: | + unzip tests/homeassistant_$SERVER_VERSION + echo "HOMEASSISTANTAPI_TOKEN=$(cat test_server/config/token.txt)" >> $GITHUB_ENV + hass -c test_server/config & + sleep 10 + deactivate + env: + SERVER_VERSION: ${{ secrets.SERVER_VERSION }} + - name: Run Test Suite + run: pytest -vvv tests + diff --git a/scripts/make_test_server b/scripts/make_test_server new file mode 100644 index 00000000..5b6085c4 --- /dev/null +++ b/scripts/make_test_server @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +echo Installing Dependencies +sudo apt-get -qq install -y python3 python3-dev python3-venv python3-pip libffi-dev libssl-dev libjpeg-dev zlib1g-dev autoconf build-essential libopenjp2-7 libtiff5 libturbojpeg0-dev tzdata + +echo Setting Up Server Directory +python -m venv test_server +. test_server/bin/activate + +echo Copying Over Control Configuration +mkdir test_server/config +cp tests/server_config.yaml test_server/config/configuration.yaml + +echo Installing Homeassistant +python -m pip install wheel setuptools --quiet +python -m pip install homeassistant --quiet + +echo Starting Homeassistant +hass -c test_server/config + +echo Zipping Folder +zip "tests/homeassistant_$(cat test_server/config/.HA_VERSION).zip" -r test_server/config + + diff --git a/tests/homeassistant_2022.6.6.zip b/tests/homeassistant_2022.6.6.zip new file mode 100644 index 0000000000000000000000000000000000000000..bb298228910121343686c272ef0a681afe46b23f GIT binary patch literal 33436 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 None: + pass From 600981c91f261a8e44546fe72e91317700ec837e Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:16:15 +0000 Subject: [PATCH 02/40] Added pytest tests --- tests/test_endpoints.py | 242 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 236 insertions(+), 6 deletions(-) diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 434a6308..7cd04e37 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,12 +1,242 @@ +# pylint: disable=redefined-outer-name import os +from typing import Generator + +import pytest +import pytest_asyncio + from homeassistant_api import Client +from homeassistant_api.models.events import Event +from homeassistant_api.models.states import State + + +@pytest.fixture(scope="function") +def cached_client() -> Generator[Client, None, None]: + """Initializes the Client and enters a cached session.""" + with Client( + os.environ["HOMEASSISTANTAPI_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"] + ) as client: + yield client + + +@pytest_asyncio.fixture(scope="function") +async def async_cached_client() -> Generator[Client, None, None]: + """Initializes the Client and enters an async cached session.""" + async with Client( + os.environ["HOMEASSISTANTAPI_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"] + ) as client: + yield client + + +def test_get_error_log(cached_client: Client) -> None: + """Tests the `GET /api/error_log` endpoint.""" + assert cached_client.get_error_log() + + +async def test_async_get_error_log(async_cached_client: Client) -> None: + """Tests the `GET /api/error_log` endpoint.""" + assert await async_cached_client.async_get_error_log() + + +def test_get_config(cached_client: Client) -> None: + """Tests the `GET /api/config` endpoint.""" + assert cached_client.get_config().get("state") == "RUNNING" + + +async def test_async_get_config(async_cached_client: Client) -> None: + """Tests the `GET /api/config` endpoint.""" + assert (await async_cached_client.async_get_config()).get("state") == "RUNNING" + + +def test_get_logbook_entries(cached_client: Client) -> None: + """Tests the `GET /api/logbook/` endpoint.""" + for entry in cached_client.get_logbook_entries(): + assert entry + + +async def test_async_get_logbook_entries(async_cached_client: Client) -> None: + """Tests the `GET /api/logbook/` endpoint.""" + async for entry in async_cached_client.async_get_logbook_entries(): + assert entry + + +def test_get_entity(cached_client: Client) -> None: + """Tests the `GET /api/states/` endpoint.""" + assert cached_client.get_entity(entity_id="sun.sun") + + +async def test_async_get_entity(async_cached_client: Client) -> None: + """Tests the `GET /api/states/` endpoint.""" + assert await async_cached_client.async_get_entity(entity_id="sun.sun") + + +def test_get_entity_histories(cached_client: Client) -> None: + """Tests the `GET /api/history/period/` endpoint.""" + for history in cached_client.get_entity_histories( + [cached_client.get_entity(entity_id="sun.sun")] + ): + for state in history.states: + assert isinstance(state, State) + + +async def test_async_get_entity_histories(async_cached_client: Client) -> None: + """Tests the `GET /api/history/period/` endpoint.""" + async for history in async_cached_client.async_get_entity_histories( + [await async_cached_client.async_get_entity(entity_id="sun.sun")] + ): + for state in history.states: + assert isinstance(state, State) + + +def test_get_rendered_template(cached_client: Client) -> None: + """Tests the `POST /api/template` endpoint.""" + rendered_template = cached_client.get_rendered_template( + 'The sun is {{ states("sun.sun").replace("_", " the ") }}.' + ) + assert rendered_template in { + "The sun is above the horizon.", + "The sun is below the horizon.", + } + + +async def test_async_get_rendered_template(async_cached_client: Client) -> None: + """Tests the `POST /api/template` endpoint.""" + rendered_template = await async_cached_client.async_get_rendered_template( + 'The sun is {{ states("sun.sun").replace("_", " the ") }}.' + ) + assert rendered_template in { + "The sun is above the horizon.", + "The sun is below the horizon.", + } + + +def test_check_api_config(cached_client: Client) -> None: + """Tests the `POST /api/config/core/check_config` endpoint.""" + assert cached_client.check_api_config() + + +async def test_async_check_api_config(async_cached_client: Client) -> None: + """Tests the `POST /api/config/core/check_config` endpoint.""" + assert await async_cached_client.async_check_api_config() + + +def test_get_entities(cached_client: Client) -> None: + """Tests the `GET /api/states` endpoint.""" + entities = cached_client.get_entities() + assert "sun" in entities + + +async def test_async_get_entities(async_cached_client: Client) -> None: + """Tests the `GET /api/states` endpoint.""" + entities = await async_cached_client.async_get_entities() + assert any(group.group_id == "sun" for group in entities) + + +def test_get_domains(cached_client: Client) -> None: + """Tests the `GET /api/services` endpoint.""" + domains = cached_client.get_domains() + assert "homeassistant" in domains + + +async def test_async_get_domains(async_cached_client: Client) -> None: + """Tests the `GET /api/services` endpoint.""" + domains = await async_cached_client.async_get_domains() + assert "homeassistant" in domains + + +def test_get_domain(cached_client: Client) -> None: + """Tests the `GET /api/services` endpoint.""" + domain = cached_client.get_domain("homeassistant") + assert domain.services + + +async def test_async_get_domain(async_cached_client: Client) -> None: + """Tests the `GET /api/services` endpoint.""" + domain = await async_cached_client.async_get_domain("homeassistant") + assert domain.services + + +def test_trigger_service(cached_client: Client) -> None: + """Tests the `POST /api/services//` endpoint.""" + notify = cached_client.get_domain("notify") + resp = notify.persistent_notification( + message="Your API Test Suite just said hello!", + title="Test Suite Notifcation", + ) + assert isinstance(resp, tuple) + + +async def test_async_trigger_service(async_cached_client: Client) -> None: + """Tests the `POST /api/services//` endpoint.""" + notify = await async_cached_client.async_get_domain("notify") + resp = await notify.persistent_notification.async_trigger( + message="Your API Test Suite just said hello!", + title="Test Suite Notifcation (Async)", + ) + assert isinstance(resp, tuple) + + +def test_get_states(cached_client: Client) -> None: + """Tests the `GET /api/states` endpoint.""" + states = cached_client.get_states() + for state in states: + assert isinstance(state, State) + + +async def test_async_get_states(async_cached_client: Client) -> None: + """Tests the `GET /api/states` endpoint.""" + states = await async_cached_client.async_get_states() + for state in states: + assert isinstance(state, State) + + +def test_get_state(cached_client: Client) -> None: + """Tests the `GET /api/states/` endpoint.""" + state = cached_client.get_state(entity_id="sun.sun") + assert state.state in {"above_horizon", "below_horizon"} + + +async def test_async_get_state(async_cached_client: Client) -> None: + """Tests the `GET /api/states/` endpoint.""" + state = await async_cached_client.async_get_state(entity_id="sun.sun") + assert state.state in {"above_horizon", "below_horizon"} + + +def test_set_state(cached_client: Client) -> None: + """Tests the `POST /api/states/` endpoint.""" + state = cached_client.set_state("beyond_our_solar_system", entity_id="sun.red_sun") + assert state.state == "beyond_our_solar_system" + + +async def test_async_set_state(async_cached_client: Client) -> None: + """Tests the `POST /api/states/` endpoint.""" + state = await async_cached_client.async_set_state( + "beyond_our_solar_system", entity_id="sun.red_sun" + ) + assert state.state == "beyond_our_solar_system" + + +def test_get_events(cached_client: Client) -> None: + """Tests the `GET /api/events` endpoint.""" + events = cached_client.get_events() + for event in events: + assert isinstance(event, Event) + + +async def test_async_get_events(async_cached_client: Client) -> None: + """Tests the `GET /api/events` endpoint.""" + events = await async_cached_client.async_get_events() + for event in events: + assert isinstance(event, Event) -CLIENT = Client( - "http://localhost:8123/api", - os.environ["HOMEASSISTANTAPI_TOKEN"] -) +def test_fire_event(cached_client: Client) -> None: + """Tests the `POST /api/events/` endpoint.""" + data = cached_client.fire_event("my_new_event", parameter="123") + assert data == "Event my_new_event fired." -def test_check_running() -> None: - pass +async def test_async_fire_event(async_cached_client: Client) -> None: + """Tests the `POST /api/events/` endpoint.""" + data = await async_cached_client.async_fire_event("my_new_event", parameter="123") + assert data == "Event my_new_event fired." From 70b705f382173ddb21089711a6d21daf972ca855 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:17:10 +0000 Subject: [PATCH 03/40] Added testing server config --- ...homeassistant_2022.6.6.zip => homeassistant.zip} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{homeassistant_2022.6.6.zip => homeassistant.zip} (100%) diff --git a/tests/homeassistant_2022.6.6.zip b/tests/homeassistant.zip similarity index 100% rename from tests/homeassistant_2022.6.6.zip rename to tests/homeassistant.zip From ab7943033310a14fa1c00f0b02220532843cb331 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:17:39 +0000 Subject: [PATCH 04/40] Added test suite to CI/CD --- .github/workflows/test_suite.yml | 16 +-- poetry.lock | 181 ++++++++++++++++++++++--------- pyproject.toml | 9 +- 3 files changed, 147 insertions(+), 59 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 2fd1494d..4b056ad7 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -11,6 +11,8 @@ on: paths: - "**.py" workflow_dispatch: + schedule: + - cron: 0 12 * * 6 jobs: code_styling: @@ -48,9 +50,8 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Install Dependencies run: | - pip install poetry homeassistant - poetry config virtualenvs.create false - poetry install --no-dev --no-root + pip install poetry + poetry install --no-root sudo apt-get -qq install -y python3 \ python3-dev python3-venv python3-pip \ libffi-dev libssl-dev libjpeg-dev zlib1g-dev \ @@ -58,13 +59,12 @@ jobs: libturbojpeg0-dev tzdata - name: Start Server run: | - unzip tests/homeassistant_$SERVER_VERSION + unzip tests/homeassistant.zip + echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api" >> $GITHUB_ENV echo "HOMEASSISTANTAPI_TOKEN=$(cat test_server/config/token.txt)" >> $GITHUB_ENV + pip install homeassistant hass -c test_server/config & sleep 10 - deactivate - env: - SERVER_VERSION: ${{ secrets.SERVER_VERSION }} - name: Run Test Suite - run: pytest -vvv tests + run: poetry run pytest -vvv tests --disable-warnings diff --git a/poetry.lock b/poetry.lock index 4273ba4c..127c232f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,12 +21,14 @@ python-versions = ">=3.6" aiodns = {version = "*", optional = true, markers = "extra == \"speedups\""} aiosignal = ">=1.1.2" async-timeout = ">=4.0.0a3,<5.0" +asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} attrs = ">=17.3.0" Brotli = {version = "*", optional = true, markers = "extra == \"speedups\""} cchardet = {version = "*", optional = true, markers = "extra == \"speedups\""} charset-normalizer = ">=2.0,<3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} yarl = ">=1.0,<2.0" [package.extras] @@ -85,7 +87,7 @@ python-versions = "*" [[package]] name = "astroid" -version = "2.11.5" +version = "2.11.6" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -93,6 +95,7 @@ python-versions = ">=3.6.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" +typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<2" @@ -104,6 +107,17 @@ category = "main" optional = false python-versions = ">=3.6" +[package.dependencies] +typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} + +[[package]] +name = "asynctest" +version = "0.13.0" +description = "Enhance the standard unittest package with features for testing asyncio libraries" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "atomicwrites" version = "1.4.0" @@ -114,21 +128,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "21.2.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "autodoc-pydantic" -version = "1.7.0" +version = "1.7.2" description = "Seamlessly integrate pydantic models in your Sphinx documentation." category = "dev" optional = false @@ -139,13 +153,13 @@ pydantic = ">=1.5" Sphinx = ">=3.4" [package.extras] -docs = ["sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (==3.2)", "sphinx-copybutton (>=0.4,<0.5)", "sphinxcontrib-mermaid (==0.7.1)"] -dev = ["sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (==3.2)", "sphinx-copybutton (>=0.4,<0.5)", "sphinxcontrib-mermaid (==0.7.1)", "pytest (>=6,<7)", "coverage (>=5,<6)", "flake8 (>=3,<4)", "tox (>=3,<4)"] +docs = ["sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (>=3,<4)", "sphinx-copybutton (>=0.4,<0.5)", "sphinxcontrib-mermaid (>=0.7,<0.8)"] +dev = ["sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (>=3,<4)", "sphinx-copybutton (>=0.4,<0.5)", "sphinxcontrib-mermaid (>=0.7,<0.8)", "pytest (>=6,<7)", "coverage (>=5,<6)", "flake8 (>=3,<4)", "tox (>=3,<4)"] test = ["pytest (>=6,<7)", "coverage (>=5,<6)"] [[package]] name = "babel" -version = "2.10.1" +version = "2.10.3" description = "Internationalization utilities" category = "dev" optional = false @@ -168,6 +182,7 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -194,6 +209,7 @@ python-versions = ">=3.7,<4.0" [package.dependencies] attrs = ">=20" +typing_extensions = {version = "*", markers = "python_version >= \"3.7\" and python_version < \"3.8\""} [[package]] name = "cchardet" @@ -205,7 +221,7 @@ python-versions = "*" [[package]] name = "certifi" -version = "2022.5.18.1" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -251,10 +267,11 @@ python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false @@ -289,7 +306,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "filelock" -version = "3.7.0" +version = "3.7.1" description = "A platform independent file lock." category = "dev" optional = false @@ -308,6 +325,7 @@ optional = false python-versions = ">=3.6" [package.dependencies] +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" @@ -349,19 +367,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.11.4" +version = "4.2.0" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -450,6 +468,7 @@ python-versions = ">=3.6" [package.dependencies] mypy-extensions = ">=0.4.3" tomli = ">=1.1.0" +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} typing-extensions = ">=3.10" [package.extras] @@ -511,6 +530,9 @@ category = "dev" optional = false python-versions = ">=3.6" +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -526,6 +548,7 @@ python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" toml = "*" @@ -644,6 +667,7 @@ python-versions = ">=3.6" atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -653,6 +677,21 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.18.3" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "python-forge" version = "18.6.0" @@ -753,7 +792,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.5.0" +version = "4.3.2" description = "Python documentation generator" category = "dev" optional = false @@ -765,7 +804,6 @@ babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.18" imagesize = "*" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" @@ -780,23 +818,23 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.920)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-autodoc-typehints" -version = "1.18.1" +version = "1.17.1" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -Sphinx = ">=4.5" +Sphinx = ">=4" [package.extras] -testing = ["covdefaults (>=2.2)", "coverage (>=6.3)", "diff-cover (>=6.4)", "nptyping (>=2)", "pytest (>=7.1)", "pytest-cov (>=3)", "sphobjinv (>=2)", "typing-extensions (>=4.1)"] -type_comments = ["typed-ast (>=1.5.2)"] +testing = ["covdefaults (>=2)", "coverage (>=6)", "diff-cover (>=6.4)", "nptyping (>=1,<2)", "pytest (>=6)", "pytest-cov (>=3)", "sphobjinv (>=2)", "typing-extensions (>=3.5)"] +type_comments = ["typed-ast (>=1.4.0)"] [[package]] name = "sphinx-rtd-theme" @@ -900,6 +938,14 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "types-docutils" version = "0.17.7" @@ -910,7 +956,7 @@ python-versions = "*" [[package]] name = "types-requests" -version = "2.27.29" +version = "2.27.31" description = "Typing stubs for requests" category = "dev" optional = false @@ -986,6 +1032,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" six = ">=1.9.0,<2" @@ -1012,6 +1059,7 @@ python-versions = ">=3.6" [package.dependencies] idna = ">=2.0" multidict = ">=4.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" @@ -1027,8 +1075,8 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" -python-versions = "^3.8" -content-hash = "8c0457596e6a147a66c63b9a4d03fcc15e77394c4abc034ec86d4769edbc0ab1" +python-versions = ">=3.7,<4.0.0" +content-hash = "8c8dc103f3fca0bdff520b6104b5656c9a3288114dd5877d80b183217f8a0221" [metadata.files] aiodns = [ @@ -1126,28 +1174,32 @@ appdirs = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] astroid = [ - {file = "astroid-2.11.5-py3-none-any.whl", hash = "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b"}, - {file = "astroid-2.11.5.tar.gz", hash = "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e"}, + {file = "astroid-2.11.6-py3-none-any.whl", hash = "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9"}, + {file = "astroid-2.11.6.tar.gz", hash = "sha256:4f933d0bf5e408b03a6feb5d23793740c27e07340605f236496cd6ce552043d6"}, ] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] +asynctest = [ + {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, + {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] autodoc-pydantic = [ - {file = "autodoc_pydantic-1.7.0-py3-none-any.whl", hash = "sha256:5bb1561647e8bc2c3c66a5331b15e4c458c5dba04c21b027a0c0306b8d39ae9b"}, - {file = "autodoc_pydantic-1.7.0.tar.gz", hash = "sha256:c48f80431fa45a531333dac96584efe8ce2177d5ffd0af21b6b4eadd6f1dd1df"}, + {file = "autodoc_pydantic-1.7.2-py3-none-any.whl", hash = "sha256:fb1cd5a2d211c0be9b5c4b516a5879cbe7c6532b6b33aebe33b66bed08a1177f"}, + {file = "autodoc_pydantic-1.7.2.tar.gz", hash = "sha256:1b987b66ef92212ea4743cae880000a24d1334d63358a3e505a6c925780248f3"}, ] babel = [ - {file = "Babel-2.10.1-py3-none-any.whl", hash = "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2"}, - {file = "Babel-2.10.1.tar.gz", hash = "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13"}, + {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, + {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] black = [ {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, @@ -1274,8 +1326,8 @@ cchardet = [ {file = "cchardet-2.1.7.tar.gz", hash = "sha256:c428b6336545053c2589f6caf24ea32276c6664cb86db817e03a94c60afa0eaf"}, ] certifi = [ - {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, - {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] cffi = [ {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, @@ -1342,8 +1394,8 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] dill = [ {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, @@ -1358,8 +1410,8 @@ docutils = [ {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] filelock = [ - {file = "filelock-3.7.0-py3-none-any.whl", hash = "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6"}, - {file = "filelock-3.7.0.tar.gz", hash = "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20"}, + {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, + {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -1439,8 +1491,8 @@ imagesize = [ {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, - {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1756,6 +1808,11 @@ pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] +pytest-asyncio = [ + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, +] python-forge = [ {file = "python_forge-18.6.0-py35-none-any.whl", hash = "sha256:bf91f9a42150d569c2e9a0d90ab60a8cbed378bdf185e5120532a3481067395c"}, ] @@ -1878,12 +1935,12 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sphinx = [ - {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, - {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, + {file = "Sphinx-4.3.2-py3-none-any.whl", hash = "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851"}, + {file = "Sphinx-4.3.2.tar.gz", hash = "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c"}, ] sphinx-autodoc-typehints = [ - {file = "sphinx_autodoc_typehints-1.18.1-py3-none-any.whl", hash = "sha256:f8f5bb7c13a9a71537dc2be2eb3b9e28a9711e2454df63587005eacf6fbac453"}, - {file = "sphinx_autodoc_typehints-1.18.1.tar.gz", hash = "sha256:07631c5f0c6641e5ba27143494aefc657e029bed3982138d659250e617f6f929"}, + {file = "sphinx_autodoc_typehints-1.17.1-py3-none-any.whl", hash = "sha256:f16491cad05a13f4825ecdf9ee4ff02925d9a3b1cf103d4d02f2f81802cce653"}, + {file = "sphinx_autodoc_typehints-1.17.1.tar.gz", hash = "sha256:844d7237d3f6280b0416f5375d9556cfd84df1945356fcc34b82e8aaacab40f3"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, @@ -1921,13 +1978,39 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +typed-ast = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] types-docutils = [ {file = "types-docutils-0.17.7.tar.gz", hash = "sha256:3d856ea26551a998c8e2c99a0bafe5e4d391811955f17dab6c9be73b0fc67b66"}, {file = "types_docutils-0.17.7-py3-none-any.whl", hash = "sha256:d35acc7e3308b464b82b54a2e641b9f132dfe0b14f199f220c58a696ce585428"}, ] types-requests = [ - {file = "types-requests-2.27.29.tar.gz", hash = "sha256:fb453b3a76a48eca66381cea8004feaaea12835e838196f5c7ac87c75c5c19ef"}, - {file = "types_requests-2.27.29-py3-none-any.whl", hash = "sha256:014f4f82db7b96c41feea9adaea30e68cd64c230eeab34b70c29bebb26ec74ac"}, + {file = "types-requests-2.27.31.tar.gz", hash = "sha256:6fab97b99fea52b9c7b466a4dd93e06bb325bc7e7420475e87831026a8dd35cc"}, + {file = "types_requests-2.27.31-py3-none-any.whl", hash = "sha256:1b6cf6a2bf57fd8018c1b636b69762900466fafddfb62e1330e092f3d4b0966a"}, ] types-simplejson = [ {file = "types-simplejson-3.17.6.tar.gz", hash = "sha256:ffa2eddd49e8e4a61d552f1f17e620d90ec872788622424f2c61ac292fbc6fa8"}, diff --git a/pyproject.toml b/pyproject.toml index e9039dbc..8727e154 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,10 +22,11 @@ version = "3.0.4" aiohttp = "^3.8.1" aiohttp-client-cache = "^0.6.1" pydantic = "<=1.9.0" -python = "^3.8" -requests = "^2.26.0" +python = ">=3.7,<4.0.0" +requests = "2.27.1" requests-cache = "^0.9.2" simplejson = "^3.17.6" +attrs = "21.2.0" [tool.poetry.dev-dependencies] black = "^22.3.0" flake8 = "^4.0.1" @@ -42,6 +43,7 @@ types-simplejson = "^3.17.3" types-toml = "^0.10.4" mypy = "^0.931" autodoc-pydantic = "^1.6.1" +pytest-asyncio = "^0.18.3" [[tool.poetry.packages]] include = "homeassistant_api" @@ -51,3 +53,6 @@ extension-pkg-whitelist = ["pydantic"] ignore-paths = ["examples"] [tool.pylint.messages_control] disable = ["invalid-name", "duplicate-code", "no-member", "too-few-public-methods", "too-many-arguments", "logging-fstring-interpolation"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" \ No newline at end of file From 7799307c6125612a7eacba929d7f12562df19437 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:18:01 +0000 Subject: [PATCH 05/40] Ignore test server files --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 4c8562f2..3fee6df5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,12 @@ report/ .replit .breakpoints +# Cache files +*.sqlite + +# Test Server +test_server/ + # Distribution / packaging .Python build/ From c3cc5c32988cb252267e178916662de272cf1a44 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:22:25 +0000 Subject: [PATCH 06/40] Merge AsyncModels with regular models --- docs/api.rst | 14 - homeassistant_api/__init__.py | 20 +- homeassistant_api/_async/__init__.py | 12 - homeassistant_api/_async/asyncclient.py | 305 -------------------- homeassistant_api/_async/models/__init__.py | 6 - homeassistant_api/_async/models/domains.py | 75 ----- homeassistant_api/_async/models/entity.py | 81 ------ homeassistant_api/_async/models/events.py | 27 -- homeassistant_api/client.py | 2 +- homeassistant_api/mixins.py | 26 -- homeassistant_api/models/domains.py | 8 + homeassistant_api/models/entity.py | 39 +++ homeassistant_api/models/events.py | 7 +- 13 files changed, 67 insertions(+), 555 deletions(-) delete mode 100644 homeassistant_api/_async/__init__.py delete mode 100644 homeassistant_api/_async/asyncclient.py delete mode 100644 homeassistant_api/_async/models/__init__.py delete mode 100644 homeassistant_api/_async/models/domains.py delete mode 100644 homeassistant_api/_async/models/entity.py delete mode 100644 homeassistant_api/_async/models/events.py diff --git a/docs/api.rst b/docs/api.rst index a12c986f..2df6f9ff 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -31,20 +31,6 @@ Data Models .. autopydantic_model:: Event - -.. automodule:: homeassistant_api._async.models - - .. autopydantic_model:: AsyncDomain - - .. autopydantic_model:: AsyncService - - .. autopydantic_model:: AsyncGroup - - .. autopydantic_model:: AsyncEntity - - .. autopydantic_model:: AsyncEvent - - Processing ----------- diff --git a/homeassistant_api/__init__.py b/homeassistant_api/__init__.py index fefc4920..02f8e944 100644 --- a/homeassistant_api/__init__.py +++ b/homeassistant_api/__init__.py @@ -1,7 +1,8 @@ -"""Imports all library stuff for convenience.""" +"""Interact with your Homeassistant Instance remotely.""" __all__ = ( + "Client", "State", "Service", "History", @@ -9,15 +10,20 @@ "Event", "Entity", "Domain", - "AsyncService", - "AsyncGroup", - "AsyncEvent", - "AsyncEntity", - "AsyncDomain", + "Processing", "LogbookEntry", + "APIConfigurationError", + "EndpointNotFoundError", + "HomeassistantAPIError", + "MalformedDataError", + "MalformedInputError", + "MethodNotAllowedError", + "ParameterMissingError", + "RequestError", + "UnauthorizedError", + "UnexpectedStatusCodeError", ) -from ._async import AsyncDomain, AsyncEntity, AsyncEvent, AsyncGroup, AsyncService from .client import Client from .errors import ( APIConfigurationError, diff --git a/homeassistant_api/_async/__init__.py b/homeassistant_api/_async/__init__.py deleted file mode 100644 index 4c30d7e1..00000000 --- a/homeassistant_api/_async/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Imports objects from the async sub-module for convenience.""" -from .asyncclient import RawAsyncClient -from .models import AsyncDomain, AsyncEntity, AsyncEvent, AsyncGroup, AsyncService - -__all__ = ( - "RawAsyncClient", - "AsyncDomain", - "AsyncEntity", - "AsyncEvent", - "AsyncGroup", - "AsyncService", -) diff --git a/homeassistant_api/_async/asyncclient.py b/homeassistant_api/_async/asyncclient.py deleted file mode 100644 index 66257675..00000000 --- a/homeassistant_api/_async/asyncclient.py +++ /dev/null @@ -1,305 +0,0 @@ -"""Module for interacting with Home Assistant asyncronously.""" -import asyncio -import logging -from datetime import datetime -from posixpath import join -from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union, cast - -import aiohttp -from aiohttp_client_cache import CachedSession - -from ..errors import APIConfigurationError, MalformedDataError, RequestError -from ..mixins import JsonProcessingMixin -from ..models import Domain, Event, History, LogbookEntry, State -from ..processing import Processing -from ..rawapi import RawWrapper -from .models import AsyncEntity, AsyncGroup - -logger = logging.getLogger(__name__) - - -class RawAsyncClient(RawWrapper, JsonProcessingMixin): - """ - The async equivalent of :class:`Client` - - :param api_url: The location of the api endpoint. e.g. :code:`http://localhost:8123/api` Required. - :param token: The refresh or long lived access token to authenticate your requests. Required. - :param global_request_kwargs: A dictionary or dict-like object of kwargs to pass to :func:`requests.request` or :meth:`aiohttp.ClientSession.request`. Optional. - """ # pylint: disable=line-too-long - - _async_session: Optional[CachedSession] = None - - async def __aenter__(self): - self._async_session = CachedSession( - expire_after=self.cache_expire_after, cache=self.cache_backend - ) - logger.debug(f"Entering cached requests session {self._async_session!r}") - await self._async_session.__aenter__() - await self.async_check_api_running() - return self - - async def __aexit__(self, cls, obj, traceback): - await self._async_session.__aexit__(cls, obj, traceback) - - # Very important request function - async def async_request( - self, - path: str, - method: str = "GET", - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Union[Dict[str, Any], List[Dict[str, Any]], str]: - """Base method for making requests to the api""" - try: - if self.global_request_kwargs is not None: - kwargs.update(self.global_request_kwargs) - if self._async_session is not None: - return await self.async_response_logic( - await self._async_session.request( - method, - self.endpoint(path), - headers=self.prepare_headers(headers), - **kwargs, - ) - ) - async with aiohttp.request( - method, - self.endpoint(path), - headers=self.prepare_headers(headers), - **kwargs, - ) as resp: - return await self.async_response_logic(resp) - except asyncio.exceptions.TimeoutError as err: - raise RequestError( - f'Home Assistant did not respond in time (timeout: {kwargs.get("timeout", 300)} sec)' - ) from err - - @staticmethod - async def async_response_logic(response) -> Any: - """Processes custom mimetype content asyncronously.""" - return await Processing(response=response).process() - - # API information methods - async def async_api_error_log(self) -> str: - """Returns the server error log as a string""" - return cast(str, await self.async_request("error_log")) - - async def async_api_config(self) -> Dict[str, Any]: - """Returns the yaml configuration of homeassistant""" - return cast(Dict[str, Any], await self.async_request("config")) - - async def async_logbook_entries( - self, - *args, - **kwargs, - ) -> AsyncGenerator[LogbookEntry, None]: - """Returns a list of logbook entries from homeassistant.""" - params, url = self.prepare_get_logbook_entry_params(*args, **kwargs) - data = await self.async_request(url, params=params) - for entry in data: - yield LogbookEntry.parse_obj(entry) - - async def async_get_entity_histories( - self, - entities: Optional[Tuple[AsyncEntity, ...]] = None, - start_timestamp: Optional[datetime] = None, - # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ - end_timestamp: Optional[datetime] = None, - minimal_state_data: bool = False, - significant_changes_only: bool = False, - ) -> AsyncGenerator[History, None]: - """ - Returns a generator of entity state histories from homeassistant. - """ - params, url = self.prepare_get_entity_histories_params( - entities=entities, - start_timestamp=start_timestamp, - end_timestamp=end_timestamp, - minimal_state_data=minimal_state_data, - significant_changes_only=significant_changes_only, - ) - data = await self.async_request( - url, - params=self.construct_params(params), - ) - for states in data: - yield History.parse_obj({"states": states}) - - async def async_get_rendered_template(self, template: str): - """Renders a given Jinja2 template string with Home Assistant context data.""" - return await self.async_request( - "template", - json=dict(template=template), - return_text=True, - method="POST", - ) - - async def async_get_discovery_info(self) -> Dict[str, Any]: - """Returns a dictionary of discovery info such as internal_url and version""" - return cast(Dict[str, Any], await self.async_request("discovery_info")) - - # API check methods - async def async_check_api_config(self) -> bool: - """Asks Home Assistant to validate its configuration file""" - res = await self.async_request("config/core/check_config", method="POST") - res = cast(Dict[Any, Any], res) - valid = {"valid": True, "invalid": False}.get( - cast( - str, - res["result"], - ), - False, - ) - if valid is False: - raise APIConfigurationError(res["errors"]) - return valid - - async def async_check_api_running(self) -> bool: - """Asks Home Assistant if its running""" - res = cast(Dict[Any, Any], await self.async_request("")) - if res.get("message", None) == "API running.": - return True - raise MalformedDataError("Server response did not return message attribute") - - # Entity methods - async def async_get_entities(self) -> Tuple[AsyncGroup, ...]: - """Fetches all entities from the api""" - entities: Dict[str, AsyncGroup] = {} - for state in await self.async_get_states(): - group_id, entity_slug = state.entity_id.split(".") - if group_id not in entities: - entities[group_id] = AsyncGroup(group_id=group_id, client=self) - entities[group_id].add_entity(entity_slug, state) - return tuple(entities.values()) - - async def async_get_entity( - self, - group_id: str = None, - entity_slug: str = None, - entity_id: str = None, - ) -> Optional[AsyncEntity]: - """Returns a Entity model for an entity_id""" - if group_id is not None and entity_slug is not None: - state = await self.async_get_state(group=group_id, slug=entity_slug) - elif entity_id is not None: - state = await self.async_get_state(entity_id=entity_id) - else: - help_msg = ( - "Use keyword arguments to pass entity_id. " - "Or you can pass the entity_group and entity_slug instead." - ) - raise ValueError( - f"Neither group and slug or entity_id provided. {help_msg}" - ) - group_id, entity_slug = state.entity_id.split(".") - group = AsyncGroup(group_id=group_id, client=self) - group.add_entity(entity_slug, state) - return group.get_entity(entity_slug) - - # Services and domain methods - async def async_get_domains(self) -> Tuple[Domain, ...]: - """Fetches all Services from the api""" - data = await self.async_request("services") - services = map( - self.process_services_json, - cast(Tuple[Dict[str, Any], ...], data), - ) - return tuple(services) - - async def async_get_domain(self, domain_id: str) -> Optional[Domain]: - """Fetchers all services under a particular domain.""" - domains = await self.async_get_domains() - for domain in domains: - if domain.domain_id == domain_id: - return domain - return None - - async def async_trigger_service( - self, - domain: str, - service: str, - **service_data: Union[Dict[str, Any], List[Any], str], - ) -> List[State]: - """Tells Home Assistant to trigger a service, returns stats changed while being called""" - data = await self.async_request( - f"services/{domain}/{service}", - method="POST", - json=service_data, - ) - return [ - self.process_state_json(state_data) - for state_data in cast(List[Dict[Any, Any]], data) - ] - - # EntityState methods - async def async_get_state( # pylint: disable=duplicate-code - self, - *, - entity_id: Optional[str] = None, - group: Optional[str] = None, - slug: Optional[str] = None, - ) -> State: - """Fetches the state of the entity specified""" - target_entity_id = self.prepare_entity_id( - group=group, - slug=slug, - entity_id=entity_id, - ) - data = await self.async_request(join("states", target_entity_id)) - return self.process_state_json(cast(Dict[Any, Any], data)) - - async def async_set_state( # pylint: disable=duplicate-code - self, - state: str, - *, - entity_id: Optional[str] = None, - group: Optional[str] = None, - slug: Optional[str] = None, - **payload, - ) -> State: - """Sets the state of the entity given (does not have to be a real entity) and returns the updated state""" - target_entity_id = self.prepare_entity_id( - group=group, - slug=slug, - entity_id=entity_id, - ) - payload.update(state=state) - data = await self.async_request( - join("states", target_entity_id), - method="POST", - json=payload, - ) - return self.process_state_json(cast(Dict[Any, Any], data)) - - async def async_get_states(self) -> List[State]: - """Gets the states of all entities within homeassistant""" - data = await self.async_request("states") - return [ - self.process_state_json(state_data) - for state_data in cast(List[Dict[Any, Any]], data) - ] - - # Event methods - async def async_get_events(self) -> Tuple[Event, ...]: - """Gets the Events that happen within homeassistant""" - data = await self.async_request("events") - if not isinstance(data, list): - events = map( - self.process_event_json, - cast(List[Dict[Any, Any]], data), - ) - return tuple(events) - raise TypeError("Received JSON data is not a list of events.") - - async def async_fire_event(self, event_type: str, **event_data) -> str: - """Fires a given event_type within homeassistant. Must be an existing event_type.""" - data = await self.async_request( - join("events", event_type), - method="POST", - json=event_data, - ) - if not isinstance(data, dict): - raise TypeError( - f"Invalid return type from API. Expected {dict!r} got {type(data)!r}" - ) - return data.get("message", "No message provided") diff --git a/homeassistant_api/_async/models/__init__.py b/homeassistant_api/_async/models/__init__.py deleted file mode 100644 index 4515703c..00000000 --- a/homeassistant_api/_async/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""The async Models for the entire library.""" -from .domains import AsyncDomain, AsyncService -from .entity import AsyncEntity, AsyncGroup -from .events import AsyncEvent - -__all__ = ("AsyncDomain", "AsyncService", "AsyncEntity", "AsyncGroup", "AsyncEvent") diff --git a/homeassistant_api/_async/models/domains.py b/homeassistant_api/_async/models/domains.py deleted file mode 100644 index 54e6274f..00000000 --- a/homeassistant_api/_async/models/domains.py +++ /dev/null @@ -1,75 +0,0 @@ -"""File for Service and Domain data models""" - -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, cast - -from pydantic import Field, validator - -from ...models import ServiceField, State, base - -if TYPE_CHECKING: - from homeassistant_api import Client - - -class AsyncDomain(base.BaseModel): - """A class representing the domain that services belong to.""" - - domain_id: str - client: "Client" = Field(exclude=True, repr=False) - services: Dict[str, "AsyncService"] = {} - - def add_service(self, service_id: str, **data) -> None: - """Registers services into a domain to be used or accessed""" - self.services.update( - { - service_id: AsyncService( - service_id=service_id, - client=self, - **data, - ) - } - ) - - def get_service(self, service_id: str) -> Optional["AsyncService"]: - """Return a Service with the given service_id, returns None if no such service exists""" - return self.services.get(service_id) - - def __getattr__(self, attr: str): - """Allows services accessible as attributes""" - if attr in self.__dict__: - return super().__getattribute__(attr) - if attr in self.services: - return self.get_service(attr) - return super().__getattribute__(attr) - - -class AsyncService(base.BaseModel): - """Class representing services from homeassistant""" - - service_id: str - domain: AsyncDomain = Field(exlude=True, repr=False) - name: Optional[str] = None - description: Optional[str] = None - fields: Optional[Dict[str, ServiceField]] = None - target: Optional[Dict[str, dict]] = None - - @classmethod - @validator("domain") - def validate_domain(cls, domain: AsyncDomain) -> AsyncDomain: - """ - Explicitly do nothing to validate the parent domain. - Elimintates recursive validation errors. - """ - return domain - - async def async_trigger(self, **service_data) -> Tuple[State, ...]: - """Triggers the service associated with this object.""" - data = await self.domain.client.async_trigger_service( - self.domain.domain_id, - self.service_id, - **service_data, - ) - states = map( - self.domain.client.process_state_json, - cast(Tuple[Dict[str, Any]], data), - ) - return tuple(states) diff --git a/homeassistant_api/_async/models/entity.py b/homeassistant_api/_async/models/entity.py deleted file mode 100644 index fc43cb36..00000000 --- a/homeassistant_api/_async/models/entity.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Module for Entity and entity Group data models""" -from datetime import datetime -from posixpath import join -from typing import TYPE_CHECKING, Any, Dict, Optional, cast - -from pydantic import Field - -from ...models import BaseModel, History, State - -if TYPE_CHECKING: - from homeassistant_api import Client - - -class AsyncGroup(BaseModel): - """Represents the groups that entities belong to""" - - group_id: str - client: "Client" = Field(exclude=True, repr=False) - entities: Dict[str, "AsyncEntity"] = {} - - def add_entity(self, entity_slug: str, state: State) -> None: - """Registers entities to this Group object""" - self.entities.update( - {entity_slug: AsyncEntity(slug=entity_slug, state=state, group=self)} - ) - - def get_entity(self, entity_slug: str) -> Optional["AsyncEntity"]: - """Returns Entity with the given name if it exists. Otherwise returns None""" - return self.entities.get(entity_slug) - - -class AsyncEntity(BaseModel): - """Represents entities inside of homeassistant""" - - slug: str - state: State - group: AsyncGroup = Field(exclude=True, repr=False) - - async def async_get_state(self) -> State: - """Asks Home Assistant for the state of the entity and sets it locally""" - state_data = await self.group.client.async_request( - join("states", self.entity_id) - ) - self.state = self.group.client.process_state_json( - cast(Dict[str, Any], state_data) - ) - return self.state - - async def async_set_state(self, state: State) -> State: - """Tells Home Assistant to set the given State object.""" - return await self.group.client.async_set_state( - self.entity_id, - group=self.group.group_id, - slug=self.slug, - **state.dict(), - ) - - @property - def entity_id(self): - """Constructs the entity_id string from its group and slug""" - return self.group.group_id + "." + self.slug - - async def async_get_history( - self, - start_timestamp: Optional[datetime] = None, - # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ - end_timestamp: Optional[datetime] = None, - minimal_state_data: bool = False, - significant_changes_only: bool = False, - ) -> Optional[History]: - """Gets the previous `State`'s of the `Entity`.""" - history: Optional[History] = None - async for history in self.group.client.async_get_entity_histories( - entities=(self,), - start_timestamp=start_timestamp, - end_timestamp=end_timestamp, - minimal_state_data=minimal_state_data, - significant_changes_only=significant_changes_only, - ): - break - return history diff --git a/homeassistant_api/_async/models/events.py b/homeassistant_api/_async/models/events.py deleted file mode 100644 index 0925b0dc..00000000 --- a/homeassistant_api/_async/models/events.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Event Model File""" -from typing import TYPE_CHECKING, Dict, cast - -from pydantic import Field - -from ...models import BaseModel - -if TYPE_CHECKING: - from homeassistant_api import Client - - -class AsyncEvent(BaseModel): - """ - Event class for Home Assistant Event Triggers - - For attribute information see the Data Science docs on Event models. - https://data.home-assistant.io/docs/events - """ - - event_type: str - listener_count: int - client: "Client" = Field(exclude=True, repr=False) - - async def async_fire(self, **event_data) -> str: - """Fires the event type in homeassistant. Ex. `on_startup`""" - data = await self.client.async_fire_event(self.event_type, **event_data) - return cast(Dict[str, str], data).get("message", "No message provided") diff --git a/homeassistant_api/client.py b/homeassistant_api/client.py index b40d8ef0..c63763ab 100644 --- a/homeassistant_api/client.py +++ b/homeassistant_api/client.py @@ -1,5 +1,5 @@ """Module containing the primary Client class.""" -from ._async import RawAsyncClient +from .rawasyncclient import RawAsyncClient from .rawclient import RawClient diff --git a/homeassistant_api/mixins.py b/homeassistant_api/mixins.py index 8d73df18..128a15df 100644 --- a/homeassistant_api/mixins.py +++ b/homeassistant_api/mixins.py @@ -1,7 +1,6 @@ """Module for processing JSON data from homeassistant.""" from typing import Any, Dict, cast -from ._async.models import AsyncDomain, AsyncEvent from .models import Domain, Event, State @@ -26,28 +25,3 @@ def process_state_json(json: Dict[str, Any]) -> State: def process_event_json(self, json: Dict[str, Any]) -> Event: """Constructs Event model from json data""" return Event(**json, client=self) - - async def async_process_services_json( - self, - json: Dict[str, Any], - ) -> AsyncDomain: - """Constructs Domain and Service models from json data""" - domain = AsyncDomain(domain_id=cast(str, json.get("domain")), client=self) - services = json.get("services") - if services is None: - raise ValueError("Missing services atrribute in passed json argument.") - for service_id, data in services.items(): - domain.add_service(service_id, **data) - return domain - - @staticmethod - async def async_process_state_json(json: Dict[str, Any]) -> State: - """Constructs State model from json data""" - return State.parse_obj(json) - - async def async_process_event_json( - self, - json: Dict[str, Any], - ) -> AsyncEvent: - """Constructs Event model from json data.""" - return AsyncEvent(**json, client=self) diff --git a/homeassistant_api/models/domains.py b/homeassistant_api/models/domains.py index 884a4bcd..cd0cc5d7 100644 --- a/homeassistant_api/models/domains.py +++ b/homeassistant_api/models/domains.py @@ -79,5 +79,13 @@ def trigger(self, **service_data) -> Tuple[State, ...]: **service_data, ) + async def async_trigger(self, **service_data) -> Tuple[State, ...]: + """Triggers the service associated with this object.""" + return await self.domain.client.async_trigger_service( + self.domain.domain_id, + self.service_id, + **service_data, + ) + def __call__(self, **service_data) -> Tuple[State, ...]: return self.trigger(**service_data) diff --git a/homeassistant_api/models/entity.py b/homeassistant_api/models/entity.py index 2a45d0c8..28ceb585 100644 --- a/homeassistant_api/models/entity.py +++ b/homeassistant_api/models/entity.py @@ -95,3 +95,42 @@ def get_history( ): break return history + + async def async_get_state(self) -> State: + """Asks Home Assistant for the state of the entity and sets it locally""" + state_data = await self.group.client.async_request( + join("states", self.entity_id) + ) + self.state = self.group.client.process_state_json( + cast(Dict[str, Any], state_data) + ) + return self.state + + async def async_set_state(self, state: State) -> State: + """Tells Home Assistant to set the given State object.""" + return await self.group.client.async_set_state( + self.entity_id, + group=self.group.group_id, + slug=self.slug, + **state.dict(), + ) + + async def async_get_history( + self, + start_timestamp: Optional[datetime] = None, + # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ + end_timestamp: Optional[datetime] = None, + minimal_state_data: bool = False, + significant_changes_only: bool = False, + ) -> Optional[History]: + """Gets the previous `State`'s of the `Entity`.""" + history: Optional[History] = None + async for history in self.group.client.async_get_entity_histories( + entities=(self,), + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + minimal_state_data=minimal_state_data, + significant_changes_only=significant_changes_only, + ): + break + return history diff --git a/homeassistant_api/models/events.py b/homeassistant_api/models/events.py index 41864bab..91302ee6 100644 --- a/homeassistant_api/models/events.py +++ b/homeassistant_api/models/events.py @@ -18,7 +18,7 @@ class Event(BaseModel): https://data.home-assistant.io/docs/events """ - event_type: str + event: str listener_count: int client: "Client" = Field(exclude=True, repr=False) @@ -26,3 +26,8 @@ def fire(self, **event_data) -> str: """Fires the corresponding event in Home Assistant.""" data = self.client.fire_event(self.event_type, **event_data) return cast(Dict[str, str], data).get("message", "No message provided") + + async def async_fire(self, **event_data) -> str: + """Fires the event type in homeassistant. Ex. `on_startup`""" + data = await self.client.async_fire_event(self.event_type, **event_data) + return cast(Dict[str, str], data).get("message", "No message provided") From ef679920af5bcaff4fafb96f0c5cf8943a6c398b Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:23:14 +0000 Subject: [PATCH 07/40] Tweak logbook model to match api --- homeassistant_api/models/logbook.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant_api/models/logbook.py b/homeassistant_api/models/logbook.py index ffcb0c88..975e7ce6 100644 --- a/homeassistant_api/models/logbook.py +++ b/homeassistant_api/models/logbook.py @@ -6,11 +6,12 @@ class LogbookEntry(BaseModel): - """Model representing""" + """Model representing entries in the Logbook.""" when: datetime name: str - entity_id: str + message: Optional[str] = None + entity_id: Optional[str] = None state: Optional[str] = None domain: Optional[str] = None context_id: Optional[str] = None From 62ebdb425a00bd6c15684faf172967fe9742c35a Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:25:51 +0000 Subject: [PATCH 08/40] Added explicit behavior of cache --- homeassistant_api/rawapi.py | 10 +- homeassistant_api/rawasyncclient.py | 338 ++++++++++++++++++++++++++++ homeassistant_api/rawclient.py | 36 +-- 3 files changed, 360 insertions(+), 24 deletions(-) create mode 100644 homeassistant_api/rawasyncclient.py diff --git a/homeassistant_api/rawapi.py b/homeassistant_api/rawapi.py index 1c258483..0ee4d3c8 100644 --- a/homeassistant_api/rawapi.py +++ b/homeassistant_api/rawapi.py @@ -26,22 +26,14 @@ def __init__( self, api_url: str, token: str, + *, global_request_kwargs: Optional[Dict[str, str]] = None, - cache_backend=None, - cache_expire_after: Optional[int] = None, ) -> None: if global_request_kwargs is None: global_request_kwargs = {} - if cache_backend is None: - cache_backend = "memory" - if cache_expire_after is None: - cache_expire_after = 30 - self.api_url = api_url self.token = token self.global_request_kwargs = global_request_kwargs - self.cache_backend = cache_backend - self.cache_expire_after = cache_expire_after if not api_url.endswith("/"): self.api_url += "/" diff --git a/homeassistant_api/rawasyncclient.py b/homeassistant_api/rawasyncclient.py new file mode 100644 index 00000000..8c68e33c --- /dev/null +++ b/homeassistant_api/rawasyncclient.py @@ -0,0 +1,338 @@ +"""Module for interacting with Home Assistant asyncronously.""" +import asyncio +import logging +from datetime import datetime +from posixpath import join +from typing import ( + Any, + AsyncGenerator, + Dict, + List, + Literal, + Optional, + Tuple, + Union, + cast, +) + +import aiohttp +from aiohttp_client_cache import CachedSession + +from homeassistant_api.models.entity import Entity, Group + +from .errors import ( + APIConfigurationError, + BadTemplateError, + MalformedDataError, + RequestError, +) +from .mixins import JsonProcessingMixin +from .models import Domain, Event, History, LogbookEntry, State +from .processing import Processing +from .rawapi import RawWrapper + +logger = logging.getLogger(__name__) + + +class RawAsyncClient(RawWrapper, JsonProcessingMixin): + """ + The async equivalent of :class:`Client` + + :param api_url: The location of the api endpoint. e.g. :code:`http://localhost:8123/api` Required. + :param token: The refresh or long lived access token to authenticate your requests. Required. + :param global_request_kwargs: A dictionary or dict-like object of kwargs to pass to :func:`requests.request` or :meth:`aiohttp.ClientSession.request`. Optional. + """ # pylint: disable=line-too-long + + def __init__( + self, + *args, + async_cache_session: Union[ + CachedSession, Literal[False], Literal[None] + ] = None, # Explicitly disable cache with async_cache_session=False + **kwargs, + ): + super().__init__(*args, **kwargs) + self.async_cache_session = ( + async_cache_session if async_cache_session is not None else CachedSession() + ) + + async def __aenter__(self): + logger.debug( + f"Entering cached async requests session {self.async_cache_session!r}" + ) + await self.async_cache_session.__aenter__() + await self.async_check_api_running() + return self + + async def __aexit__(self, cls, obj, traceback): + logger.debug(f"Exiting async requests session {self.async_cache_session!r}") + await self.async_cache_session.__aexit__(cls, obj, traceback) + + # Very important request function + async def async_request( + self, + path: str, + method: str = "GET", + headers: Optional[Dict[str, str]] = None, + **kwargs, + ) -> Union[Dict[str, Any], List[Dict[str, Any]], str]: + """Base method for making requests to the api""" + try: + if self.global_request_kwargs is not None: + kwargs.update(self.global_request_kwargs) + if self.async_cache_session: + return await self.async_response_logic( + await self.async_cache_session.request( + method, + self.endpoint(path), + headers=self.prepare_headers(headers), + **kwargs, + ) + ) + async with aiohttp.request( + method, + self.endpoint(path), + headers=self.prepare_headers(headers), + **kwargs, + ) as resp: + return await self.async_response_logic(resp) + except asyncio.exceptions.TimeoutError as err: + raise RequestError( + f'Home Assistant did not respond in time (timeout: {kwargs.get("timeout", 300)} sec)' + ) from err + + @staticmethod + async def async_response_logic(response) -> Any: + """Processes custom mimetype content asyncronously.""" + return await Processing(response=response).process() + + # API information methods + async def async_get_error_log(self) -> str: + """Returns the server error log as a string""" + return cast(str, await self.async_request("error_log")) + + async def async_get_config(self) -> Dict[str, Any]: + """Returns the yaml configuration of homeassistant""" + return cast(Dict[str, Any], await self.async_request("config")) + + async def async_get_logbook_entries( + self, + *args, + **kwargs, + ) -> AsyncGenerator[LogbookEntry, None]: + """Returns a list of logbook entries from homeassistant.""" + params, url = self.prepare_get_logbook_entry_params(*args, **kwargs) + data = await self.async_request(url, params=params) + for entry in data: + yield LogbookEntry.parse_obj(entry) + + async def async_get_entity_histories( + self, + entities: Optional[Tuple[Entity, ...]] = None, + start_timestamp: Optional[datetime] = None, + # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ + end_timestamp: Optional[datetime] = None, + minimal_state_data: bool = False, + significant_changes_only: bool = False, + ) -> AsyncGenerator[History, None]: + """ + Returns a generator of entity state histories from homeassistant. + """ + params, url = self.prepare_get_entity_histories_params( + entities=entities, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + minimal_state_data=minimal_state_data, + significant_changes_only=significant_changes_only, + ) + data = await self.async_request( + url, + params=self.construct_params(params), + ) + for states in data: + yield History.parse_obj({"states": states}) + + async def async_get_rendered_template(self, template: str): + """Renders a given Jinja2 template string with Home Assistant context data.""" + try: + return await self.async_request( + "template", + json=dict(template=template), + method="POST", + ) + except RequestError as err: + raise BadTemplateError( + "Your template is invalid. " + "Try debugging it in the developer tools page of homeassistant." + ) from err + + async def async_get_discovery_info(self) -> Dict[str, Any]: + """Returns a dictionary of discovery info such as internal_url and version""" + raise DeprecationWarning( + "This endpoint has been removed from homeassistant. This function is to be removed in future release." + ) + + # API check methods + async def async_check_api_config(self) -> bool: + """Asks Home Assistant to validate its configuration file""" + res = await self.async_request("config/core/check_config", method="POST") + res = cast(Dict[Any, Any], res) + valid = {"valid": True, "invalid": False}.get( + cast( + str, + res["result"], + ), + False, + ) + if valid is False: + raise APIConfigurationError(res["errors"]) + return valid + + async def async_check_api_running(self) -> bool: + """Asks Home Assistant if its running""" + res = cast(Dict[Any, Any], await self.async_request("")) + if res.get("message", None) == "API running.": + return True + raise MalformedDataError("Server response did not return message attribute") + + # Entity methods + async def async_get_entities(self) -> Tuple[Group, ...]: + """Fetches all entities from the api""" + entities: Dict[str, Group] = {} + for state in await self.async_get_states(): + group_id, entity_slug = state.entity_id.split(".") + if group_id not in entities: + entities[group_id] = Group(group_id=group_id, client=self) + entities[group_id].add_entity(entity_slug, state) + return tuple(entities.values()) + + async def async_get_entity( + self, + group_id: str = None, + entity_slug: str = None, + entity_id: str = None, + ) -> Optional[Entity]: + """Returns a Entity model for an entity_id""" + if group_id is not None and entity_slug is not None: + state = await self.async_get_state(group=group_id, slug=entity_slug) + elif entity_id is not None: + state = await self.async_get_state(entity_id=entity_id) + else: + help_msg = ( + "Use keyword arguments to pass entity_id. " + "Or you can pass the entity_group and entity_slug instead." + ) + raise ValueError( + f"Neither group and slug or entity_id provided. {help_msg}" + ) + group_id, entity_slug = state.entity_id.split(".") + group = Group(group_id=group_id, client=self) + group.add_entity(entity_slug, state) + return group.get_entity(entity_slug) + + # Services and domain methods + async def async_get_domains(self) -> Dict[str, Domain]: + """Fetches all services from the api""" + data = await self.async_request("services") + domains = map( + self.process_services_json, + cast(Tuple[Dict[str, Any], ...], data), + ) + return {domain.domain_id: domain for domain in domains} + + async def async_get_domain(self, domain_id: str) -> Optional[Domain]: + """Fetches all services under a particular domain.""" + domains = await self.async_get_domains() + return domains.get(domain_id) + + async def async_trigger_service( + self, + domain: str, + service: str, + **service_data: Union[Dict[str, Any], List[Any], str], + ) -> Tuple[State, ...]: + """Tells Home Assistant to trigger a service, returns stats changed while being called""" + data = await self.async_request( + f"services/{domain}/{service}", + method="POST", + json=service_data, + ) + return tuple(map(self.process_state_json, cast(List[Dict[Any, Any]], data))) + + # EntityState methods + async def async_get_state( # pylint: disable=duplicate-code + self, + *, + entity_id: Optional[str] = None, + group: Optional[str] = None, + slug: Optional[str] = None, + ) -> State: + """Fetches the state of the entity specified""" + target_entity_id = self.prepare_entity_id( + group=group, + slug=slug, + entity_id=entity_id, + ) + data = await self.async_request(join("states", target_entity_id)) + return self.process_state_json(cast(Dict[Any, Any], data)) + + async def async_set_state( # pylint: disable=duplicate-code + self, + state: str, + *, + entity_id: Optional[str] = None, + group: Optional[str] = None, + slug: Optional[str] = None, + **payload, + ) -> State: + """Sets the state of the entity given (does not have to be a real entity) and returns the updated state""" + target_entity_id = self.prepare_entity_id( + group=group, + slug=slug, + entity_id=entity_id, + ) + payload.update(state=state) + data = await self.async_request( + join("states", target_entity_id), + method="POST", + json=payload, + ) + return self.process_state_json(cast(Dict[Any, Any], data)) + + async def async_get_states(self) -> List[State]: + """Gets the states of all entities within homeassistant""" + data = await self.async_request("states") + return [ + self.process_state_json(state_data) + for state_data in cast(List[Dict[Any, Any]], data) + ] + + # Event methods + async def async_get_events(self) -> Tuple[Event, ...]: + """Gets the internal events that happen within homeassistant.""" + data = await self.async_request("events") + if isinstance(data, list): + return tuple( + map( + self.process_event_json, + cast(List[Dict[str, Any]], data), + ) + ) + raise TypeError("Received JSON data is not a list of events.") + + async def async_fire_event(self, event_type: str, **event_data) -> str: + """Fires a given event_type within homeassistant. Must be an existing event_type.""" + data = await self.async_request( + join("events", event_type), + method="POST", + json=event_data, + ) + if not isinstance(data, dict): + raise TypeError( + f"Invalid return type from API. Expected {dict!r} got {type(data)!r}" + ) + return data.get("message", "No message provided") + + async def async_get_components(self) -> Tuple[str, ...]: + """Returns a tuple of all registered components.""" + return tuple(await self.async_request("components")) diff --git a/homeassistant_api/rawclient.py b/homeassistant_api/rawclient.py index 268291af..dec10f27 100644 --- a/homeassistant_api/rawclient.py +++ b/homeassistant_api/rawclient.py @@ -26,23 +26,29 @@ class RawClient(RawWrapper, JsonProcessingMixin): :param global_request_kwargs: Kwargs to pass to :func:`requests.request` or :meth:`aiohttp.ClientSession.request`. Optional. """ # pylint: disable=line-too-long - _session: Optional[CachedSession] = None + def __init__( + self, + *args, + cache_session: Union[ + CachedSession, Literal[False], Literal[None] + ] = None, # Explicitly disable cache with cache_session=False + **kwargs, + ): + super().__init__(*args, **kwargs) + self.cache_session = ( + cache_session if cache_session is not None else CachedSession() + ) def __enter__(self): - self._session = CachedSession( - expire_after=self.cache_expire_after, - backend=self.cache_backend, - ) - logger.debug(f"Entering cached requests session {self._session!r}") - self._session.__enter__() + logger.debug(f"Entering cached requests session {self.cache_session!r}") + self.cache_session.__enter__() self.check_api_running() self.check_api_config() return self def __exit__(self, *args): - logger.debug(f"Exiting requests session {self._session!r}") - self._session.__exit__() - self._session = None + logger.debug(f"Exiting requests session {self.cache_session!r}") + self.cache_session.__exit__(*args) def request( self, @@ -56,8 +62,8 @@ def request( if self.global_request_kwargs is not None: kwargs.update(self.global_request_kwargs) logger.debug(f"{method} request to {self.endpoint(path)}") - if self._session is not None: - resp = self._session.request( + if self.cache_session: + resp = self.cache_session.request( method, self.endpoint(path), headers=self.prepare_headers(headers), @@ -82,15 +88,15 @@ def response_logic(cls, response: requests.Response) -> Union[dict, list, str]: return Processing(response=response).process() # API information methods - def api_error_log(self) -> str: + def get_error_log(self) -> str: """Returns the server error log as a string.""" return cast(str, self.request("error_log")) - def api_config(self) -> Dict[str, Any]: + def get_config(self) -> Dict[str, Any]: """Returns the yaml configuration of homeassistant.""" return cast(Dict[str, Any], self.request("config")) - def logbook_entries( + def get_logbook_entries( self, *args, **kwargs, From 0225938bf7032a654f2bf70f24a40e020b32d54d Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:44:04 +0000 Subject: [PATCH 09/40] Added get components method --- homeassistant_api/rawclient.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant_api/rawclient.py b/homeassistant_api/rawclient.py index dec10f27..c2f9ca60 100644 --- a/homeassistant_api/rawclient.py +++ b/homeassistant_api/rawclient.py @@ -303,4 +303,7 @@ def fire_event(self, event_type: str, **event_data) -> str: method="POST", json=event_data, ) - return cast(dict, data).get("message", "No message provided") + + def get_components(self) -> Tuple[str, ...]: + """Returns a tuple of all registered components.""" + return tuple(self.request("components")) From 3d253e3f9fc1ddd236ae4a0a97ba9351a6340c81 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 01:44:32 +0000 Subject: [PATCH 10/40] Polished existing endpoints to conform to tests --- homeassistant_api/errors.py | 4 ++ homeassistant_api/rawclient.py | 77 ++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/homeassistant_api/errors.py b/homeassistant_api/errors.py index 9bfc10d4..e8ee820f 100644 --- a/homeassistant_api/errors.py +++ b/homeassistant_api/errors.py @@ -12,6 +12,10 @@ def __init__(self, body: str) -> None: super().__init__(f"Bad Request: {body}") +class BadTemplateError(HomeassistantAPIError): + """Error raised when User sends bad template to homeassistant.""" + + class MalformedDataError(HomeassistantAPIError): """Error raised when data from api is not formatted as JSON""" diff --git a/homeassistant_api/rawclient.py b/homeassistant_api/rawclient.py index c2f9ca60..0b394b66 100644 --- a/homeassistant_api/rawclient.py +++ b/homeassistant_api/rawclient.py @@ -3,12 +3,12 @@ import logging from datetime import datetime from posixpath import join -from typing import Any, Dict, Generator, List, Optional, Tuple, Union, cast +from typing import Any, Dict, Generator, List, Literal, Optional, Tuple, Union, cast import requests from requests_cache import CachedSession -from .errors import APIConfigurationError, RequestError +from .errors import APIConfigurationError, BadTemplateError, RequestError from .mixins import JsonProcessingMixin from .models import Domain, Entity, Event, Group, History, LogbookEntry, State from .processing import Processing @@ -136,26 +136,32 @@ def get_entity_histories( def get_rendered_template(self, template: str) -> str: """ Renders a Jinja2 template with Home Assistant context data. - See https://developers.home-assistant.io/docs/api/rest/. + See https://www.home-assistant.io/docs/configuration/templating. """ - return cast( - str, - self.request( - "template", - json=dict(template=template), - return_text=True, - method="POST", - ), - ) + try: + return cast( + str, + self.request( + "template", + json=dict(template=template), + method="POST", + ), + ) + except RequestError as err: + raise BadTemplateError( + "Your template is invalid. " + "Try debugging it in the developer tools page of homeassistant." + ) from err def get_discovery_info(self) -> Dict[str, Any]: """Returns a dictionary of discovery info such as internal_url and version""" - res = self.request("discovery_info") - return cast(dict, res) + raise DeprecationWarning( + "This endpoint has been removed from homeassistant. This function is to be removed in future release." + ) # API check methods def check_api_config(self) -> bool: - """Asks Home Assistant to validate its configuration file""" + """Asks Home Assistant to validate its configuration file.""" res = cast(dict, self.request("config/core/check_config", method="POST")) valid = {"valid": True, "invalid": False}.get(res["result"], False) if valid is False: @@ -163,7 +169,7 @@ def check_api_config(self) -> bool: return valid def check_api_running(self) -> bool: - """Asks Home Assistant if its running""" + """Asks Home Assistant if it is running.""" res = self.request("") if cast(dict, res).get("message", None) == "API running.": return True @@ -205,22 +211,18 @@ def get_entity( return group.get_entity(cast(str, entity_slug)) # Services and domain methods - def get_domains(self) -> Tuple[Domain, ...]: - """Fetches all Services from the api""" + def get_domains(self) -> Dict[str, Domain]: + """Fetches all Services from the API""" data = self.request("services") - services = map( + domains = map( self.process_services_json, cast(Tuple[Dict[str, Any], ...], data), ) - return tuple(services) + return {domain.domain_id: domain for domain in domains} def get_domain(self, domain_id: str) -> Optional[Domain]: - """Fetchers all services under a particular domain.""" - domains = self.get_domains() - for domain in domains: - if domain.domain_id == domain_id: - return domain - return None + """Fetches all services under a particular domain.""" + return self.get_domains().get(domain_id) def trigger_service( self, @@ -228,14 +230,13 @@ def trigger_service( service: str, **service_data, ) -> Tuple[State, ...]: - """Tells Home Assistant to trigger a service, returns stats changed while being called""" + """Tells Home Assistant to trigger a service, returns all states changed while in the process of being called.""" data = self.request( join("services", domain + "/" + service), method="POST", json=service_data, ) - states = map(self.process_state_json, cast(List[Dict[str, Any]], data)) - return tuple(states) + return tuple(map(self.process_state_json, cast(List[Dict[str, Any]], data))) # EntityState methods def get_state( # pylint: disable=duplicate-code @@ -265,7 +266,7 @@ def set_state( # pylint: disable=duplicate-code ) -> State: """ This method sets the representation of a device within Home Assistant and will not communicate with the actual device. - To communicate with the device, use :py:meth:`homeassistant_api.Service.trigger` or :py:meth:`homeassistant_api.AsyncService.trigger` + To communicate with the device, use :py:meth:`homeassistant_api.Service.trigger` or :py:meth:`homeassistant_api.Service.async_trigger` """ entity_id = self.prepare_entity_id( group=group, @@ -290,19 +291,23 @@ def get_states(self) -> Tuple[State, ...]: def get_events(self) -> Tuple[Event, ...]: """Gets the Events that happen within homeassistant""" data = self.request("events") - events = map( - self.process_event_json, - cast(Tuple[Dict[str, Any], ...], data), - ) - return tuple(events) + if isinstance(data, list): + return tuple( + map( + self.process_event_json, + cast(List[Dict[str, Any]], data), + ) + ) + raise TypeError("Received JSON data is not a list of events.") - def fire_event(self, event_type: str, **event_data) -> str: + def fire_event(self, event_type: str, **event_data) -> Optional[str]: """Fires a given event_type within homeassistant. Must be an existing event_type.""" data = self.request( join("events", event_type), method="POST", json=event_data, ) + return cast(dict, data).get("message") def get_components(self) -> Tuple[str, ...]: """Returns a tuple of all registered components.""" From af1e6af93a6a0bc15e6a49dc11133398aa0a73bb Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 19:08:55 +0000 Subject: [PATCH 11/40] Test Suite CI/CD syntax fix --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 4b056ad7..dfcb3bc0 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v2 - name: Checkout uses: actions/checkout@v2 with: From 593a7f7e11c1bdbe0aaf7059bb577c39295a373e Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 19:40:42 +0000 Subject: [PATCH 12/40] Linted changes --- .github/workflows/test_suite.yml | 3 +-- homeassistant_api/models/base.py | 2 +- homeassistant_api/models/entity.py | 4 +--- homeassistant_api/models/events.py | 4 ++-- homeassistant_api/processing.py | 6 +++--- homeassistant_api/rawapi.py | 9 ++------- homeassistant_api/rawasyncclient.py | 10 ++++++---- homeassistant_api/rawclient.py | 9 +++++---- pyproject.toml | 18 ++++++++++++------ tests/test_endpoints.py | 20 ++++++++++++-------- 10 files changed, 45 insertions(+), 40 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index dfcb3bc0..0e78801a 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -66,5 +66,4 @@ jobs: hass -c test_server/config & sleep 10 - name: Run Test Suite - run: poetry run pytest -vvv tests --disable-warnings - + run: poetry run pytest -vvv tests --disable-warnings \ No newline at end of file diff --git a/homeassistant_api/models/base.py b/homeassistant_api/models/base.py index bca15cbd..a6209f16 100644 --- a/homeassistant_api/models/base.py +++ b/homeassistant_api/models/base.py @@ -6,7 +6,7 @@ class BaseModel(PydanticBaseModel): """Base model that all Library Models inherit from.""" - class Config: + class Config: # pylint: disable=too-few-public-methods """Pydantic config class for all library models.""" arbitrary_types_allowed = True diff --git a/homeassistant_api/models/entity.py b/homeassistant_api/models/entity.py index 28ceb585..56380533 100644 --- a/homeassistant_api/models/entity.py +++ b/homeassistant_api/models/entity.py @@ -36,9 +36,7 @@ def get_entity(self, entity_slug: str) -> Optional["Entity"]: def __getattr__(self, key: str): if key in self.entities: return self.get_entity(key) - return super(object, self).__getattribute__( # type: ignore[misc] # pylint: disable=bad-super-call - key - ) + return super().__getattribute__(key) class Entity(BaseModel): diff --git a/homeassistant_api/models/events.py b/homeassistant_api/models/events.py index 91302ee6..75f45fa7 100644 --- a/homeassistant_api/models/events.py +++ b/homeassistant_api/models/events.py @@ -24,10 +24,10 @@ class Event(BaseModel): def fire(self, **event_data) -> str: """Fires the corresponding event in Home Assistant.""" - data = self.client.fire_event(self.event_type, **event_data) + data = self.client.fire_event(self.event, **event_data) return cast(Dict[str, str], data).get("message", "No message provided") async def async_fire(self, **event_data) -> str: """Fires the event type in homeassistant. Ex. `on_startup`""" - data = await self.client.async_fire_event(self.event_type, **event_data) + data = await self.client.async_fire_event(self.event, **event_data) return cast(Dict[str, str], data).get("message", "No message provided") diff --git a/homeassistant_api/processing.py b/homeassistant_api/processing.py index 478a5e08..26101398 100644 --- a/homeassistant_api/processing.py +++ b/homeassistant_api/processing.py @@ -52,14 +52,14 @@ def process_content(self, _async: bool): mimetype = self.response.headers.get("content-type", "text/plain") # type: ignore[arg-type] for processor in self._processors.get(mimetype, ()): if not _async ^ inspect.iscoroutinefunction(processor): - logger.debug(f"Using processor {processor!r} on {self.response!r}") + logger.debug("Using processor %r on %r", processor, self.response) return processor(self.response) if _async: raise ProcessorNotFoundError( - f"No async response processor registered for mimetype {mimetype!r}" + f"No async response processor registered for mimetype {mimetype!r}." ) raise ProcessorNotFoundError( - f"No non-async response processor found for mimetype {mimetype!r}" + f"No non-async response processor found for mimetype {mimetype!r}." ) def process(self) -> Any: diff --git a/homeassistant_api/rawapi.py b/homeassistant_api/rawapi.py index 0ee4d3c8..223c2df5 100644 --- a/homeassistant_api/rawapi.py +++ b/homeassistant_api/rawapi.py @@ -3,17 +3,12 @@ import re from datetime import datetime from posixpath import join -from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union +from typing import Dict, Optional, Tuple, Union from urllib.parse import quote as url_quote from .const import DATE_FMT from .models import Entity -if TYPE_CHECKING: - from ._async.models.entity import AsyncEntity -else: - AsyncEntity = None # pylint: disable=invalid-name - class RawWrapper: """Builds, and makes requests to the API""" @@ -99,7 +94,7 @@ def prepare_entity_id( @staticmethod def prepare_get_entity_histories_params( - entities: Optional[Tuple[Union[Entity, AsyncEntity], ...]] = None, + entities: Optional[Tuple[Entity, ...]] = None, start_timestamp: Optional[datetime] = None, # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ end_timestamp: Optional[datetime] = None, diff --git a/homeassistant_api/rawasyncclient.py b/homeassistant_api/rawasyncclient.py index 8c68e33c..381915f4 100644 --- a/homeassistant_api/rawasyncclient.py +++ b/homeassistant_api/rawasyncclient.py @@ -58,14 +58,14 @@ def __init__( async def __aenter__(self): logger.debug( - f"Entering cached async requests session {self.async_cache_session!r}" + "Entering cached async requests session %r", self.async_cache_session ) await self.async_cache_session.__aenter__() await self.async_check_api_running() return self async def __aexit__(self, cls, obj, traceback): - logger.debug(f"Exiting async requests session {self.async_cache_session!r}") + logger.debug("Exiting async requests session %r", self.async_cache_session) await self.async_cache_session.__aexit__(cls, obj, traceback) # Very important request function @@ -166,7 +166,8 @@ async def async_get_rendered_template(self, template: str): "Try debugging it in the developer tools page of homeassistant." ) from err - async def async_get_discovery_info(self) -> Dict[str, Any]: + @staticmethod + async def async_get_discovery_info() -> Dict[str, Any]: """Returns a dictionary of discovery info such as internal_url and version""" raise DeprecationWarning( "This endpoint has been removed from homeassistant. This function is to be removed in future release." @@ -335,4 +336,5 @@ async def async_fire_event(self, event_type: str, **event_data) -> str: async def async_get_components(self) -> Tuple[str, ...]: """Returns a tuple of all registered components.""" - return tuple(await self.async_request("components")) + data = await self.async_request("components") + return tuple(cast(List[str], data)) diff --git a/homeassistant_api/rawclient.py b/homeassistant_api/rawclient.py index 0b394b66..c87266a6 100644 --- a/homeassistant_api/rawclient.py +++ b/homeassistant_api/rawclient.py @@ -40,14 +40,14 @@ def __init__( ) def __enter__(self): - logger.debug(f"Entering cached requests session {self.cache_session!r}") + logger.debug("Entering cached requests session %r.", self.cache_session) self.cache_session.__enter__() self.check_api_running() self.check_api_config() return self def __exit__(self, *args): - logger.debug(f"Exiting requests session {self.cache_session!r}") + logger.debug("Exiting requests session %r", self.cache_session) self.cache_session.__exit__(*args) def request( @@ -61,7 +61,7 @@ def request( try: if self.global_request_kwargs is not None: kwargs.update(self.global_request_kwargs) - logger.debug(f"{method} request to {self.endpoint(path)}") + logger.debug("%s request to %s", method, self.endpoint(path)) if self.cache_session: resp = self.cache_session.request( method, @@ -153,7 +153,8 @@ def get_rendered_template(self, template: str) -> str: "Try debugging it in the developer tools page of homeassistant." ) from err - def get_discovery_info(self) -> Dict[str, Any]: + @staticmethod + def get_discovery_info() -> Dict[str, Any]: """Returns a dictionary of discovery info such as internal_url and version""" raise DeprecationWarning( "This endpoint has been removed from homeassistant. This function is to be removed in future release." diff --git a/pyproject.toml b/pyproject.toml index 8727e154..474e8345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,12 @@ build-backend = "poetry.core.masonry.api" requires = ["poetry-core>=1.0.0"] -[tool] [tool.bandit] skips = ["B105"] + [tool.isort] profile = "black" + [tool.poetry] authors = ["GrandMoff100 "] description = "Python Wrapper for Homeassistant's REST API" @@ -18,6 +19,8 @@ readme = "README.md" include = ["homeassistant_api/py.typed"] repository = "https://github.com/GrandMoff100/HomeAssistantAPI" version = "3.0.4" +packages = [{ include = "homeassistant_api" }] + [tool.poetry.dependencies] aiohttp = "^3.8.1" aiohttp-client-cache = "^0.6.1" @@ -26,7 +29,7 @@ python = ">=3.7,<4.0.0" requests = "2.27.1" requests-cache = "^0.9.2" simplejson = "^3.17.6" -attrs = "21.2.0" + [tool.poetry.dev-dependencies] black = "^22.3.0" flake8 = "^4.0.1" @@ -44,15 +47,18 @@ types-toml = "^0.10.4" mypy = "^0.931" autodoc-pydantic = "^1.6.1" pytest-asyncio = "^0.18.3" +attrs = "21.2.0" -[[tool.poetry.packages]] -include = "homeassistant_api" -[tool.pylint] [tool.pylint.master] extension-pkg-whitelist = ["pydantic"] ignore-paths = ["examples"] + [tool.pylint.messages_control] -disable = ["invalid-name", "duplicate-code", "no-member", "too-few-public-methods", "too-many-arguments", "logging-fstring-interpolation"] +disable = [ + "duplicate-code", + "too-many-public-methods", + "too-many-arguments", +] [tool.pytest.ini_options] asyncio_mode = "auto" \ No newline at end of file diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 7cd04e37..edcad46d 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,6 +1,6 @@ # pylint: disable=redefined-outer-name import os -from typing import Generator +from typing import AsyncGenerator, Generator import pytest import pytest_asyncio @@ -20,7 +20,7 @@ def cached_client() -> Generator[Client, None, None]: @pytest_asyncio.fixture(scope="function") -async def async_cached_client() -> Generator[Client, None, None]: +async def async_cached_client() -> AsyncGenerator[Client, None]: """Initializes the Client and enters an async cached session.""" async with Client( os.environ["HOMEASSISTANTAPI_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"] @@ -72,18 +72,18 @@ async def test_async_get_entity(async_cached_client: Client) -> None: def test_get_entity_histories(cached_client: Client) -> None: """Tests the `GET /api/history/period/` endpoint.""" - for history in cached_client.get_entity_histories( - [cached_client.get_entity(entity_id="sun.sun")] - ): + sun = cached_client.get_entity(entity_id="sun.sun") + assert sun is not None + for history in cached_client.get_entity_histories((sun,)): for state in history.states: assert isinstance(state, State) async def test_async_get_entity_histories(async_cached_client: Client) -> None: """Tests the `GET /api/history/period/` endpoint.""" - async for history in async_cached_client.async_get_entity_histories( - [await async_cached_client.async_get_entity(entity_id="sun.sun")] - ): + sun = await async_cached_client.async_get_entity(entity_id="sun.sun") + assert sun is not None + async for history in async_cached_client.async_get_entity_histories((sun,)): for state in history.states: assert isinstance(state, State) @@ -147,18 +147,21 @@ async def test_async_get_domains(async_cached_client: Client) -> None: def test_get_domain(cached_client: Client) -> None: """Tests the `GET /api/services` endpoint.""" domain = cached_client.get_domain("homeassistant") + assert domain is not None assert domain.services async def test_async_get_domain(async_cached_client: Client) -> None: """Tests the `GET /api/services` endpoint.""" domain = await async_cached_client.async_get_domain("homeassistant") + assert domain is not None assert domain.services def test_trigger_service(cached_client: Client) -> None: """Tests the `POST /api/services//` endpoint.""" notify = cached_client.get_domain("notify") + assert notify is not None resp = notify.persistent_notification( message="Your API Test Suite just said hello!", title="Test Suite Notifcation", @@ -169,6 +172,7 @@ def test_trigger_service(cached_client: Client) -> None: async def test_async_trigger_service(async_cached_client: Client) -> None: """Tests the `POST /api/services//` endpoint.""" notify = await async_cached_client.async_get_domain("notify") + assert notify is not None resp = await notify.persistent_notification.async_trigger( message="Your API Test Suite just said hello!", title="Test Suite Notifcation (Async)", From 41602338b5ac852ef1dc364d37c08b35b55535ea Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 19:52:07 +0000 Subject: [PATCH 13/40] Add cwd to PYTHONPATH --- .github/workflows/test_suite.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 0e78801a..ff5a7a78 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -66,4 +66,6 @@ jobs: hass -c test_server/config & sleep 10 - name: Run Test Suite - run: poetry run pytest -vvv tests --disable-warnings \ No newline at end of file + run: | + export PYTHONPATH=$PWD + poetry run pytest -vvv tests --disable-warnings \ No newline at end of file From 72bc872999a98f466a30c084fe6794217e49fb19 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 20:44:37 +0000 Subject: [PATCH 14/40] Fixed non-preserved server between steps --- .github/workflows/test_suite.yml | 13 ++++++------- tests/server_config.yaml | 4 +--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index ff5a7a78..df8b71f4 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -57,15 +57,14 @@ jobs: libffi-dev libssl-dev libjpeg-dev zlib1g-dev \ autoconf build-essential libopenjp2-7 libtiff5 \ libturbojpeg0-dev tzdata - - name: Start Server + - name: Start Server and Run Test Suite run: | - unzip tests/homeassistant.zip - echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api" >> $GITHUB_ENV - echo "HOMEASSISTANTAPI_TOKEN=$(cat test_server/config/token.txt)" >> $GITHUB_ENV pip install homeassistant + unzip tests/homeassistant.zip + export HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" hass -c test_server/config & sleep 10 - - name: Run Test Suite - run: | export PYTHONPATH=$PWD - poetry run pytest -vvv tests --disable-warnings \ No newline at end of file + poetry run pytest tests --disable-warnings + env: + HOMEASSISTANTAPI_URL: "http://localhost:8123/api" \ No newline at end of file diff --git a/tests/server_config.yaml b/tests/server_config.yaml index 1415c1f0..18960898 100644 --- a/tests/server_config.yaml +++ b/tests/server_config.yaml @@ -1,4 +1,4 @@ -# Gets renamed to configuration.yaml in the Docker container +# Gets renamed to configuration.yaml in the testing volume person: sun: @@ -18,9 +18,7 @@ notify: logger: default: info - http: trusted_proxies: - 127.0.0.1 use_x_forwarded_for: true - From 049f2836795d2114c1aae03dc2df2c2d16769a76 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 20:59:06 +0000 Subject: [PATCH 15/40] Ping server instead of sleep --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index df8b71f4..03223fd1 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -63,7 +63,7 @@ jobs: unzip tests/homeassistant.zip export HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" hass -c test_server/config & - sleep 10 + wget http://localhost:8123/api --timeout=60 export PYTHONPATH=$PWD poetry run pytest tests --disable-warnings env: From 8010cfbd64ed5f3dfd7e5601438f2c8fe75c5691 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 21:04:00 +0000 Subject: [PATCH 16/40] Fixed declare and assign sepparately --- .github/workflows/test_suite.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 03223fd1..0beec076 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -61,10 +61,12 @@ jobs: run: | pip install homeassistant unzip tests/homeassistant.zip - export HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" + HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" + PYTHONPATH=$PWD hass -c test_server/config & wget http://localhost:8123/api --timeout=60 - export PYTHONPATH=$PWD + export HOMEASSISTANTAPI_TOKEN + export PYTHONPATH poetry run pytest tests --disable-warnings env: HOMEASSISTANTAPI_URL: "http://localhost:8123/api" \ No newline at end of file From cca3bc2143a85027553f97f70aa6e4c74cc7641c Mon Sep 17 00:00:00 2001 From: GrandMoff100 Date: Wed, 22 Jun 2022 21:05:28 +0000 Subject: [PATCH 17/40] [MegaLinter] Apply linters fixes --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 0beec076..37d184ca 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -69,4 +69,4 @@ jobs: export PYTHONPATH poetry run pytest tests --disable-warnings env: - HOMEASSISTANTAPI_URL: "http://localhost:8123/api" \ No newline at end of file + HOMEASSISTANTAPI_URL: "http://localhost:8123/api" From b36d7a95af45d52f7f668e483221588dcfa100a8 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 22:47:37 +0000 Subject: [PATCH 18/40] Assert server running --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 37d184ca..d2c2709e 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -63,7 +63,7 @@ jobs: unzip tests/homeassistant.zip HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" PYTHONPATH=$PWD - hass -c test_server/config & + hass -c test_server/config wget http://localhost:8123/api --timeout=60 export HOMEASSISTANTAPI_TOKEN export PYTHONPATH From 822babc540b7b823ed4020c454c05c21940e720c Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 22 Jun 2022 23:12:25 +0000 Subject: [PATCH 19/40] Increase the timeout for packages install --- .github/workflows/test_suite.yml | 23 ++++++---- poetry.lock | 78 ++++++++++++++++---------------- pyproject.toml | 1 - 3 files changed, 52 insertions(+), 50 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index d2c2709e..8a24c4ed 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -42,29 +42,32 @@ jobs: code_functionality: runs-on: ubuntu-latest steps: - - name: Setup Python - uses: actions/setup-python@v2 - name: Checkout uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.head.sha }} + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.9" - name: Install Dependencies run: | - pip install poetry - poetry install --no-root - sudo apt-get -qq install -y python3 \ - python3-dev python3-venv python3-pip \ - libffi-dev libssl-dev libjpeg-dev zlib1g-dev \ + python3 -m venv . + source bin/activate + python3 -m pip install poetry homeassistant + python3 -m poetry install --no-root --no-dev + sudo apt-get -qq install -y libffi-dev \ + libssl-dev libjpeg-dev zlib1g-dev \ autoconf build-essential libopenjp2-7 libtiff5 \ libturbojpeg0-dev tzdata - name: Start Server and Run Test Suite run: | - pip install homeassistant unzip tests/homeassistant.zip HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" PYTHONPATH=$PWD - hass -c test_server/config - wget http://localhost:8123/api --timeout=60 + hass -c test_server/config & + wget http://localhost:8123/api --timeout=300 + # Wait 5 minutes for server to install packages and start. export HOMEASSISTANTAPI_TOKEN export PYTHONPATH poetry run pytest tests --disable-warnings diff --git a/poetry.lock b/poetry.lock index 127c232f..add4e4ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -128,17 +128,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "autodoc-pydantic" @@ -564,7 +564,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycares" -version = "4.1.2" +version = "4.2.0" description = "Python interface for c-ares" category = "main" optional = false @@ -1076,7 +1076,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0.0" -content-hash = "8c8dc103f3fca0bdff520b6104b5656c9a3288114dd5877d80b183217f8a0221" +content-hash = "03408a135a7895dbf85044f0798fd65a97bc98bb8b61b7d7a2dc065cff01f7e3" [metadata.files] aiodns = [ @@ -1190,8 +1190,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] autodoc-pydantic = [ {file = "autodoc_pydantic-1.7.2-py3-none-any.whl", hash = "sha256:fb1cd5a2d211c0be9b5c4b516a5879cbe7c6532b6b33aebe33b66bed08a1177f"}, @@ -1711,37 +1711,37 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycares = [ - {file = "pycares-4.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71b99b9e041ae3356b859822c511f286f84c8889ec9ed1fbf6ac30fb4da13e4c"}, - {file = "pycares-4.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c000942f5fc64e6e046aa61aa53b629b576ba11607d108909727c3c8f211a157"}, - {file = "pycares-4.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b0e50ddc78252f2e2b6b5f2c73e5b2449dfb6bea7a5a0e21dfd1e2bcc9e17382"}, - {file = "pycares-4.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6831e963a910b0a8cbdd2750ffcdf5f2bb0edb3f53ca69ff18484de2cc3807c4"}, - {file = "pycares-4.1.2-cp310-cp310-win32.whl", hash = "sha256:ad7b28e1b6bc68edd3d678373fa3af84e39d287090434f25055d21b4716b2fc6"}, - {file = "pycares-4.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:27a6f09dbfb69bb79609724c0f90dfaa7c215876a7cd9f12d585574d1f922112"}, - {file = "pycares-4.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e5a060f5fa90ae245aa99a4a8ad13ec39c2340400de037c7e8d27b081e1a3c64"}, - {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:056330275dea42b7199494047a745e1d9785d39fb8c4cd469dca043532240b80"}, - {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0aa897543a786daba74ec5e19638bd38b2b432d179a0e248eac1e62de5756207"}, - {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cbceaa9b2c416aa931627466d3240aecfc905c292c842252e3d77b8630072505"}, - {file = "pycares-4.1.2-cp36-cp36m-win32.whl", hash = "sha256:112e1385c451069112d6b5ea1f9c378544f3c6b89882ff964e9a64be3336d7e4"}, - {file = "pycares-4.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c6680f7fdc0f1163e8f6c2a11d11b9a0b524a61000d2a71f9ccd410f154fb171"}, - {file = "pycares-4.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a41a2baabcd95266db776c510d349d417919407f03510fc87ac7488730d913"}, - {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a810d01c9a426ee8b0f36969c2aef5fb966712be9d7e466920beb328cd9cefa3"}, - {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b266cec81dcea2c3efbbd3dda00af8d7eb0693ae9e47e8706518334b21f27d4a"}, - {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8319afe4838e09df267c421ca93da408f770b945ec6217dda72f1f6a493e37e4"}, - {file = "pycares-4.1.2-cp37-cp37m-win32.whl", hash = "sha256:4d5da840aa0d9b15fa51107f09270c563a348cb77b14ae9653d0bbdbe326fcc2"}, - {file = "pycares-4.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5632f21d92cc0225ba5ff906e4e5dec415ef0b3df322c461d138190681cd5d89"}, - {file = "pycares-4.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8fd1ff17a26bb004f0f6bb902ba7dddd810059096ae0cc3b45e4f5be46315d19"}, - {file = "pycares-4.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439799be4b7576e907139a7f9b3c8a01b90d3e38af4af9cd1fc6c1ee9a42b9e6"}, - {file = "pycares-4.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:40079ed58efa91747c50aac4edf8ecc7e570132ab57dc0a4030eb0d016a6cab8"}, - {file = "pycares-4.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e190471a015f8225fa38069617192e06122771cce2b169ac7a60bfdbd3d4ab2"}, - {file = "pycares-4.1.2-cp38-cp38-win32.whl", hash = "sha256:2b837315ed08c7df009b67725fe1f50489e99de9089f58ec1b243dc612f172aa"}, - {file = "pycares-4.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:c7eba3c8354b730a54d23237d0b6445a2f68570fa68d0848887da23a3f3b71f3"}, - {file = "pycares-4.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f5f84fe9f83eab9cd68544b165b74ba6e3412d029cc9ab20098d9c332869fc5"}, - {file = "pycares-4.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569eef8597b5e02b1bc4644b9f272160304d8c9985357d7ecfcd054da97c0771"}, - {file = "pycares-4.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e1489aa25d14dbf7176110ead937c01176ed5a0ebefd3b092bbd6b202241814c"}, - {file = "pycares-4.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dc942692fca0e27081b7bb414bb971d34609c80df5e953f6d0c62ecc8019acd9"}, - {file = "pycares-4.1.2-cp39-cp39-win32.whl", hash = "sha256:ed71dc4290d9c3353945965604ef1f6a4de631733e9819a7ebc747220b27e641"}, - {file = "pycares-4.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:ec00f3594ee775665167b1a1630edceefb1b1283af9ac57480dba2fb6fd6c360"}, - {file = "pycares-4.1.2.tar.gz", hash = "sha256:03490be0e7b51a0c8073f877bec347eff31003f64f57d9518d419d9369452837"}, + {file = "pycares-4.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:48c733b8087f618d64b05c5807264ea94987b599cf37688ba59d23c57e197ff1"}, + {file = "pycares-4.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a491bb6c9c6420f3547e89898902e6f67a11ba42ff39026cc94aa238bcc2e67"}, + {file = "pycares-4.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e6bbf663d89a93df0386050db430a26d98155d1c6d9f1e522ac8b01d38016c10"}, + {file = "pycares-4.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1857631b448925aabc2b306baa27fbedb27ca49036ce5c083ae21eef7850adfa"}, + {file = "pycares-4.2.0-cp310-cp310-win32.whl", hash = "sha256:b076d5d8a3f94bc38efeb7a5ac9772190e13515a510f96d600d58b12d25b2ae7"}, + {file = "pycares-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:496cc499980d9be6b151f5028aef9e735a4881d8c2eaa32b2562678dce463787"}, + {file = "pycares-4.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9d057445ebc381398fb26fb87e6b9d2ed416e58074710a3e7feb6115b8a28ac"}, + {file = "pycares-4.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae732c15c5bb5a381c521e3eb4daf56175e890f4fa1fddf2fc2349a778a94ed0"}, + {file = "pycares-4.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:67b9495aebd575c18cf0371636057fdaf198f8b60e7bf1ec7fdd511154094f94"}, + {file = "pycares-4.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3cf407fba0ab0f21417676d480926dd438e12a0134d2e50e6164b0b4822ff63"}, + {file = "pycares-4.2.0-cp36-cp36m-win32.whl", hash = "sha256:a56eea0cf117edffb51479e2acda15b1b02a33a225838708828a5c1010333ba1"}, + {file = "pycares-4.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9070d507349d8b75c3f3e81b7d519555cd009391ae2b8fce7505753058e6cd08"}, + {file = "pycares-4.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1aadb7ebaa6da649ba3e665898aebd9f1ab57ece9af6c0d1c089074bf9a822a"}, + {file = "pycares-4.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c4a6410e01f401982b7bc9d20756a9e1549bab1cc431eb9a7a26193168895b0"}, + {file = "pycares-4.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a1c0cc79c442eecd0beeaf2bcd91541a0ff366b43df5bb061d57dbe8b5d04c38"}, + {file = "pycares-4.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc24ee402697d4babe3a3652af6ede7fdd18f8d33a691d62c1e33131435f66e2"}, + {file = "pycares-4.2.0-cp37-cp37m-win32.whl", hash = "sha256:e7b396e074efca974b211378f2bfb864f9be0465ba75f6a8851d8b115d928d79"}, + {file = "pycares-4.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:84d6572dff420631758ebdffc13283895e7fb04a54823641fa3830cdcb6f4743"}, + {file = "pycares-4.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:23d3f272b9f6ea80e488de76ea93e0edde418d545e86562b9e9094952a6ce658"}, + {file = "pycares-4.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88709762179daa858b494f66f5ad35cf9e4be39893a565aa24148a237d05f3cd"}, + {file = "pycares-4.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad97e6db69a8b825fa4590cddbd00b00441fc16bfbebf3a37f6f9b6f72d9380d"}, + {file = "pycares-4.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e0cffbf241c0384a6b56e2cddfb7bbd7b9830954edd756f685ad2b36e54ee107"}, + {file = "pycares-4.2.0-cp38-cp38-win32.whl", hash = "sha256:bce6e64b85aad08dbeb6f92aecd53d856ac534fb70e6d3a5baa7cda8d2301c16"}, + {file = "pycares-4.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:2539b9e20c52d9daedfa5f52f84ba6ea8cb351236fcda6034ab5422f0be1d171"}, + {file = "pycares-4.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2707044e62ae32d816141e99bb2cfbba02700f4b5ffebbd7ba3fb2070af9a5ff"}, + {file = "pycares-4.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3178be9e269795216376220d8a847bf30497fcf415821d58659b07dc2873e9b2"}, + {file = "pycares-4.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:de97ae19883b3f49f7c75d60292d49a21d06e64113ed69b5c6d5eba8ae4ef60a"}, + {file = "pycares-4.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:45cc3feae3335b70cd4ec7447cd6ef9c11c7d9aa6731153cb87bae4db69fed8d"}, + {file = "pycares-4.2.0-cp39-cp39-win32.whl", hash = "sha256:8401526f945c96210f81887ad38d5415113f149719893006fc148862fbf4c128"}, + {file = "pycares-4.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:50a50b6c55919563745b0b242aa54eeb727696351ac187156d2d0ab9614fd726"}, + {file = "pycares-4.2.0.tar.gz", hash = "sha256:b286649597791cd53072b2f3383cc38fc14a8ab016b78cb04bdcaa6ecce3b8ce"}, ] pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, diff --git a/pyproject.toml b/pyproject.toml index 474e8345..115f6b0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ types-toml = "^0.10.4" mypy = "^0.931" autodoc-pydantic = "^1.6.1" pytest-asyncio = "^0.18.3" -attrs = "21.2.0" [tool.pylint.master] extension-pkg-whitelist = ["pydantic"] From dabc3adf24752800a7102c87999e2f0c5d193942 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 20:48:07 +0000 Subject: [PATCH 20/40] Integration heck has been overcome --- .github/workflows/test_suite.yml | 33 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 8a24c4ed..d6d057ec 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -52,24 +52,31 @@ jobs: python-version: "3.9" - name: Install Dependencies run: | - python3 -m venv . - source bin/activate - python3 -m pip install poetry homeassistant - python3 -m poetry install --no-root --no-dev sudo apt-get -qq install -y libffi-dev \ libssl-dev libjpeg-dev zlib1g-dev \ autoconf build-essential libopenjp2-7 libtiff5 \ libturbojpeg0-dev tzdata - - name: Start Server and Run Test Suite + - name: Unpack Testing Server Config run: | unzip tests/homeassistant.zip - HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" - PYTHONPATH=$PWD + echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api" >> $GITHUB_ENV + echo 'HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)"' >> $GITHUB_ENV + echo 'PYTHONPATH="$PWD"' >> $GITHUB_ENV + - name: Initialize Testing Server and Run Suite + run: | + # Install Dependencies + python3 -m poetry install --no-root --no-dev + python3 -m venv . + . bin/activate + python3 -m pip install poetry homeassistant + + # Start Server hass -c test_server/config & - wget http://localhost:8123/api --timeout=300 - # Wait 5 minutes for server to install packages and start. - export HOMEASSISTANTAPI_TOKEN - export PYTHONPATH + deactivate + + # Wait at least 60 seconds for server to install packages and start. + sleep 60 + wget $HOMEASSISTANTAPI_URL -O /dev/null -o /dev/null --no-check-certificate + + # Run Test Suite poetry run pytest tests --disable-warnings - env: - HOMEASSISTANTAPI_URL: "http://localhost:8123/api" From 243515c442653ad70112101c97e11381d4b75019 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 20:49:33 +0000 Subject: [PATCH 21/40] Ahhhhhh added missing pip install poetry line --- .github/workflows/test_suite.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index d6d057ec..468de8f6 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -65,10 +65,11 @@ jobs: - name: Initialize Testing Server and Run Suite run: | # Install Dependencies + python3 -m pip install poetry python3 -m poetry install --no-root --no-dev python3 -m venv . . bin/activate - python3 -m pip install poetry homeassistant + python3 -m pip install homeassistant # Start Server hass -c test_server/config & From 1dbb7614a85291c0b3e81318aaa41efe6f73b91e Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 20:55:05 +0000 Subject: [PATCH 22/40] Silenced pip --- .github/workflows/test_suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 468de8f6..f41b0f38 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -65,11 +65,11 @@ jobs: - name: Initialize Testing Server and Run Suite run: | # Install Dependencies - python3 -m pip install poetry + python3 -m pip install poetry -q python3 -m poetry install --no-root --no-dev python3 -m venv . . bin/activate - python3 -m pip install homeassistant + python3 -m pip install homeassistant -q # Start Server hass -c test_server/config & From fcfe3097ba0ff85d7d4971837e5466651b7c6fb0 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 20:56:41 +0000 Subject: [PATCH 23/40] Added debug messages --- .github/workflows/test_suite.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index f41b0f38..0c30c107 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -65,19 +65,24 @@ jobs: - name: Initialize Testing Server and Run Suite run: | # Install Dependencies + echo "::debug::Install Project Dependencies" python3 -m pip install poetry -q python3 -m poetry install --no-root --no-dev + + echo "::debug::Install Home Assistant Dependencies" python3 -m venv . . bin/activate python3 -m pip install homeassistant -q # Start Server + echo "::debug::Starting the Testing Server" hass -c test_server/config & deactivate # Wait at least 60 seconds for server to install packages and start. + echo "::debug::Waiting for Server to Start" sleep 60 - wget $HOMEASSISTANTAPI_URL -O /dev/null -o /dev/null --no-check-certificate # Run Test Suite + echo "::debug::Running Test Suite" poetry run pytest tests --disable-warnings From 0b4d0af6ce78a21065bfa29bfe6e586196dc99a0 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 20:59:39 +0000 Subject: [PATCH 24/40] Added log line grouping --- .github/workflows/test_suite.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 0c30c107..b36b098b 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -64,25 +64,27 @@ jobs: echo 'PYTHONPATH="$PWD"' >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | - # Install Dependencies - echo "::debug::Install Project Dependencies" - python3 -m pip install poetry -q + echo "::group:: Install Project Dependencies" + python3 -m pip install poetry python3 -m poetry install --no-root --no-dev + echo "::endgroup::" - echo "::debug::Install Home Assistant Dependencies" + echo "::group::Install Home Assistant Dependencies" python3 -m venv . . bin/activate python3 -m pip install homeassistant -q + echo "::endgroup::" - # Start Server - echo "::debug::Starting the Testing Server" + echo "::group::Starting the Testing Server" hass -c test_server/config & deactivate # Wait at least 60 seconds for server to install packages and start. echo "::debug::Waiting for Server to Start" sleep 60 + echo "::endgroup::" # Run Test Suite - echo "::debug::Running Test Suite" + echo "::group::Running Test Suite" poetry run pytest tests --disable-warnings + echo "::endgroup::" From 6f132694b857a5ede454699d9a106284deb3018d Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 21:03:57 +0000 Subject: [PATCH 25/40] Enabled dev dependency install --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index b36b098b..c8791f5c 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -66,7 +66,7 @@ jobs: run: | echo "::group:: Install Project Dependencies" python3 -m pip install poetry - python3 -m poetry install --no-root --no-dev + python3 -m poetry install echo "::endgroup::" echo "::group::Install Home Assistant Dependencies" From e6a509ce26411a35f2b0015d15b0688398fdc684 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 21:14:05 +0000 Subject: [PATCH 26/40] Linted ci/cd files --- .github/workflows/test_suite.yml | 12 +++++++----- .mega-linter.yml | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index c8791f5c..2a78efc2 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -59,12 +59,14 @@ jobs: - name: Unpack Testing Server Config run: | unzip tests/homeassistant.zip - echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api" >> $GITHUB_ENV - echo 'HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)"' >> $GITHUB_ENV - echo 'PYTHONPATH="$PWD"' >> $GITHUB_ENV + echo ' + HOMEASSISTANTAPI_URL=http://localhost:8123/api; + HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)"; + PYTHONPATH="$PWD" + ' >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | - echo "::group:: Install Project Dependencies" + echo "::group::Install Project Dependencies" python3 -m pip install poetry python3 -m poetry install echo "::endgroup::" @@ -72,7 +74,7 @@ jobs: echo "::group::Install Home Assistant Dependencies" python3 -m venv . . bin/activate - python3 -m pip install homeassistant -q + python3 -m pip install homeassistant echo "::endgroup::" echo "::group::Starting the Testing Server" diff --git a/.mega-linter.yml b/.mega-linter.yml index 471c573a..5a1e3031 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -11,4 +11,5 @@ DISABLE_LINTERS: SHOW_ELAPSED_TIME: true FILEIO_REPORTER: true RST_FILTER_REGEX_EXCLUDE: "(:resource:`.+`)" +ACTION_ACTIONLINT_ARGUMENTS: "-ignore spellcheck" # DISABLE_ERRORS: true # Uncomment if you want MegaLinter to detect errors but not block CI to pass From 717141a3c2ac5624045d1bb7880828696862d86d Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 21:20:49 +0000 Subject: [PATCH 27/40] Fixed more linting issues --- .github/workflows/test_suite.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 2a78efc2..fee871ec 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -16,6 +16,7 @@ on: jobs: code_styling: + name: "Code Styling" runs-on: ubuntu-latest steps: - name: Setup Python @@ -40,6 +41,7 @@ jobs: - name: Run PyLint run: pylint homeassistant_api code_functionality: + name: "Code Functionality" runs-on: ubuntu-latest steps: - name: Checkout @@ -59,11 +61,9 @@ jobs: - name: Unpack Testing Server Config run: | unzip tests/homeassistant.zip - echo ' - HOMEASSISTANTAPI_URL=http://localhost:8123/api; - HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)"; - PYTHONPATH="$PWD" - ' >> $GITHUB_ENV + echo "HOMEASSISTANTAPI_URL='http://localhost:8123/api'; + HOMEASSISTANTAPI_TOKEN='$(cat test_server/config/token.txt)'; + PYTHONPATH='$PWD'" >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | echo "::group::Install Project Dependencies" From 328ac83e28ade4d954803baf074d2f60b2314d7a Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 21:49:47 +0000 Subject: [PATCH 28/40] Added correct envs to final step --- .github/workflows/test_suite.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index fee871ec..67a43f73 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -66,6 +66,7 @@ jobs: PYTHONPATH='$PWD'" >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | + echo "::group::Install Project Dependencies" python3 -m pip install poetry python3 -m poetry install @@ -90,3 +91,7 @@ jobs: echo "::group::Running Test Suite" poetry run pytest tests --disable-warnings echo "::endgroup::" + env: + HOMEASSISTANTAPI_URL: {{ env.HOMEASSISTANTAPI_URL }} + HOMEASSISTANTAPI_TOKEN: {{ env.HOMEASSISTANTAPI_TOKEN }} + PYTHONPATH: {{ env.PYTHONPATH }} From 9aa5abea9239d3ee2866395dedda0ea4dc4b1b76 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 21:53:23 +0000 Subject: [PATCH 29/40] Escape double quotes in :66:25 --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 67a43f73..30151493 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -63,7 +63,7 @@ jobs: unzip tests/homeassistant.zip echo "HOMEASSISTANTAPI_URL='http://localhost:8123/api'; HOMEASSISTANTAPI_TOKEN='$(cat test_server/config/token.txt)'; - PYTHONPATH='$PWD'" >> $GITHUB_ENV + PYTHONPATH=\"$PWD\"" >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | From 0eba24b1bbba941e0a4729d0722394f40915cde7 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 22:06:04 +0000 Subject: [PATCH 30/40] Lint yml files --- .github/workflows/codeql-analysis.yml | 2 ++ .github/workflows/mega-linter.yml | 1 + .github/workflows/python-publish.yml | 4 ++-- .github/workflows/test_suite.yml | 9 +++++---- .gitpod.yml | 9 --------- .mega-linter.yml | 2 ++ .pre-commit-config.yaml | 2 ++ 7 files changed, 14 insertions(+), 15 deletions(-) delete mode 100644 .gitpod.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51b80569..639fd0eb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,3 +1,4 @@ +--- # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # @@ -72,3 +73,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 +--- \ No newline at end of file diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml index 99e79f0b..966cac84 100644 --- a/.github/workflows/mega-linter.yml +++ b/.github/workflows/mega-linter.yml @@ -82,3 +82,4 @@ jobs: with: branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} commit_message: "[MegaLinter] Apply linters fixes" +--- \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1578057b..a6a4c96c 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,11 +1,10 @@ +--- name: Upload Python Package - on: release: types: - published workflow_dispatch: - jobs: deploy: runs-on: ubuntu-latest @@ -25,3 +24,4 @@ jobs: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: poetry publish --build -u "$TWINE_USERNAME" -p "$TWINE_PASSWORD" +--- \ No newline at end of file diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 30151493..d5822ec7 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -1,5 +1,5 @@ +--- name: Code Standards - on: push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) paths: @@ -92,6 +92,7 @@ jobs: poetry run pytest tests --disable-warnings echo "::endgroup::" env: - HOMEASSISTANTAPI_URL: {{ env.HOMEASSISTANTAPI_URL }} - HOMEASSISTANTAPI_TOKEN: {{ env.HOMEASSISTANTAPI_TOKEN }} - PYTHONPATH: {{ env.PYTHONPATH }} + HOMEASSISTANTAPI_URL: "${{ env.HOMEASSISTANTAPI_URL }}" + HOMEASSISTANTAPI_TOKEN: "${{ env.HOMEASSISTANTAPI_TOKEN }}" + PYTHONPATH: "${{ env.PYTHONPATH }}" +--- \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index fe7b4c4b..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,9 +0,0 @@ -tasks: - - init: | - pip install poetry - poetry config virtualenvs.create false - poetry install -ports: - - port: 8000 - visibility: public - onOpen: ignore diff --git a/.mega-linter.yml b/.mega-linter.yml index 5a1e3031..07ff584b 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -1,3 +1,4 @@ +--- # Configuration file for MegaLinter # See all available variables at https://megalinter.github.io/configuration/ and in linters documentation @@ -13,3 +14,4 @@ FILEIO_REPORTER: true RST_FILTER_REGEX_EXCLUDE: "(:resource:`.+`)" ACTION_ACTIONLINT_ARGUMENTS: "-ignore spellcheck" # DISABLE_ERRORS: true # Uncomment if you want MegaLinter to detect errors but not block CI to pass +--- \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 851e4a1c..9dc48c0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +--- repos: - repo: https://github.com/pre-commit/mirrors-mypy hooks: @@ -27,3 +28,4 @@ repos: hooks: - id: pylint language: system +--- \ No newline at end of file From 54ecb5dbd2a19bb19049977be71499fd5ae31275 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 22:13:31 +0000 Subject: [PATCH 31/40] Reverse said linting --- .github/workflows/codeql-analysis.yml | 2 -- .github/workflows/mega-linter.yml | 2 -- .github/workflows/python-publish.yml | 4 ++-- .github/workflows/test_suite.yml | 2 -- .mega-linter.yml | 1 - .pre-commit-config.yaml | 4 +--- homeassistant_api/rawapi.py | 2 +- 7 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 639fd0eb..51b80569 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,3 @@ ---- # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # @@ -73,4 +72,3 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ---- \ No newline at end of file diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml index 966cac84..da59d32a 100644 --- a/.github/workflows/mega-linter.yml +++ b/.github/workflows/mega-linter.yml @@ -1,4 +1,3 @@ ---- # MegaLinter GitHub Action configuration file # More info at https://megalinter.github.io name: MegaLinter @@ -82,4 +81,3 @@ jobs: with: branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} commit_message: "[MegaLinter] Apply linters fixes" ---- \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index a6a4c96c..1578057b 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,10 +1,11 @@ ---- name: Upload Python Package + on: release: types: - published workflow_dispatch: + jobs: deploy: runs-on: ubuntu-latest @@ -24,4 +25,3 @@ jobs: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: poetry publish --build -u "$TWINE_USERNAME" -p "$TWINE_PASSWORD" ---- \ No newline at end of file diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index d5822ec7..6160c62d 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -1,4 +1,3 @@ ---- name: Code Standards on: push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) @@ -95,4 +94,3 @@ jobs: HOMEASSISTANTAPI_URL: "${{ env.HOMEASSISTANTAPI_URL }}" HOMEASSISTANTAPI_TOKEN: "${{ env.HOMEASSISTANTAPI_TOKEN }}" PYTHONPATH: "${{ env.PYTHONPATH }}" ---- \ No newline at end of file diff --git a/.mega-linter.yml b/.mega-linter.yml index 07ff584b..6163fb5e 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -14,4 +14,3 @@ FILEIO_REPORTER: true RST_FILTER_REGEX_EXCLUDE: "(:resource:`.+`)" ACTION_ACTIONLINT_ARGUMENTS: "-ignore spellcheck" # DISABLE_ERRORS: true # Uncomment if you want MegaLinter to detect errors but not block CI to pass ---- \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9dc48c0f..56446f3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,3 @@ ---- repos: - repo: https://github.com/pre-commit/mirrors-mypy hooks: @@ -27,5 +26,4 @@ repos: rev: "v2.12.2" hooks: - id: pylint - language: system ---- \ No newline at end of file + language: system \ No newline at end of file diff --git a/homeassistant_api/rawapi.py b/homeassistant_api/rawapi.py index 223c2df5..aa0b0dcd 100644 --- a/homeassistant_api/rawapi.py +++ b/homeassistant_api/rawapi.py @@ -141,7 +141,7 @@ def prepare_get_logbook_entry_params( if start_timestamp is not None: if isinstance(start_timestamp, datetime): formatted_timestamp = start_timestamp.strftime(DATE_FMT) - url = join("logbook/", formatted_timestamp) + url = join("logbook/", formatted_timestamp) else: url = "logbook" return params, url From f56f012b329434be8c5a09639097543b8742731d Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 22:22:14 +0000 Subject: [PATCH 32/40] Please lint --- .github/workflows/test_suite.yml | 10 +++++----- homeassistant_api/rawapi.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 6160c62d..307a9848 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -61,8 +61,8 @@ jobs: run: | unzip tests/homeassistant.zip echo "HOMEASSISTANTAPI_URL='http://localhost:8123/api'; - HOMEASSISTANTAPI_TOKEN='$(cat test_server/config/token.txt)'; - PYTHONPATH=\"$PWD\"" >> $GITHUB_ENV + HOMEASSISTANTAPI_TOKEN='$(cat test_server/config/token.txt)'; + " >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | @@ -88,9 +88,9 @@ jobs: # Run Test Suite echo "::group::Running Test Suite" + export PYTHONPATH=. poetry run pytest tests --disable-warnings echo "::endgroup::" env: - HOMEASSISTANTAPI_URL: "${{ env.HOMEASSISTANTAPI_URL }}" - HOMEASSISTANTAPI_TOKEN: "${{ env.HOMEASSISTANTAPI_TOKEN }}" - PYTHONPATH: "${{ env.PYTHONPATH }}" + HOMEASSISTANTAPI_URL: ${{ env.HOMEASSISTANTAPI_URL }} + HOMEASSISTANTAPI_TOKEN: ${{ env.HOMEASSISTANTAPI_TOKEN }} diff --git a/homeassistant_api/rawapi.py b/homeassistant_api/rawapi.py index aa0b0dcd..74ea3d4f 100644 --- a/homeassistant_api/rawapi.py +++ b/homeassistant_api/rawapi.py @@ -140,8 +140,7 @@ def prepare_get_logbook_entry_params( params.update(end_time=end_timestamp) if start_timestamp is not None: if isinstance(start_timestamp, datetime): - formatted_timestamp = start_timestamp.strftime(DATE_FMT) - url = join("logbook/", formatted_timestamp) + url = join("logbook/", start_timestamp.strftime(DATE_FMT)) else: url = "logbook" return params, url From 5a4ca603f232638558eb0402e7a6cf01ba1b9f34 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 22:25:13 +0000 Subject: [PATCH 33/40] Remove quotes from env variables --- .github/workflows/test_suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 307a9848..b9f2c41c 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -60,8 +60,8 @@ jobs: - name: Unpack Testing Server Config run: | unzip tests/homeassistant.zip - echo "HOMEASSISTANTAPI_URL='http://localhost:8123/api'; - HOMEASSISTANTAPI_TOKEN='$(cat test_server/config/token.txt)'; + echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api; + HOMEASSISTANTAPI_TOKEN=$(cat test_server/config/token.txt); " >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | From 070b5a4f26a682012c36e4d5b8f2bb40c231801b Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 22:32:44 +0000 Subject: [PATCH 34/40] Fixed variable issues --- .github/workflows/test_suite.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index b9f2c41c..4447241f 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -60,12 +60,10 @@ jobs: - name: Unpack Testing Server Config run: | unzip tests/homeassistant.zip - echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api; - HOMEASSISTANTAPI_TOKEN=$(cat test_server/config/token.txt); - " >> $GITHUB_ENV + echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api" >> $GITHUB_ENV + echo "HOMEASSISTANTAPI_TOKEN=$(cat test_server/config/token.txt)" >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | - echo "::group::Install Project Dependencies" python3 -m pip install poetry python3 -m poetry install From da27d58ba2301ba9f562f3b148fe0cb178d8d814 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 22:39:08 +0000 Subject: [PATCH 35/40] Move env variables into final step --- .github/workflows/test_suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 4447241f..218a2685 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -60,8 +60,6 @@ jobs: - name: Unpack Testing Server Config run: | unzip tests/homeassistant.zip - echo "HOMEASSISTANTAPI_URL=http://localhost:8123/api" >> $GITHUB_ENV - echo "HOMEASSISTANTAPI_TOKEN=$(cat test_server/config/token.txt)" >> $GITHUB_ENV - name: Initialize Testing Server and Run Suite run: | echo "::group::Install Project Dependencies" @@ -87,6 +85,8 @@ jobs: # Run Test Suite echo "::group::Running Test Suite" export PYTHONPATH=. + export HOMEASSISTANTAPI_URL="http://localhost:8123/api" + export HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" poetry run pytest tests --disable-warnings echo "::endgroup::" env: From 10f825f3cf5621e74507fd07c41b1b13d1843bb8 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 23 Jun 2022 22:42:14 +0000 Subject: [PATCH 36/40] Maybe? --- .github/workflows/test_suite.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 218a2685..3705cd44 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -84,9 +84,10 @@ jobs: # Run Test Suite echo "::group::Running Test Suite" + TOKEN = "$(cat test_server/config/token.txt)"" export PYTHONPATH=. export HOMEASSISTANTAPI_URL="http://localhost:8123/api" - export HOMEASSISTANTAPI_TOKEN="$(cat test_server/config/token.txt)" + export HOMEASSISTANTAPI_TOKEN="$TOKEN" poetry run pytest tests --disable-warnings echo "::endgroup::" env: From 41676eecd42af7c433b2411f0f180c1fb79b4068 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 24 Jun 2022 09:02:35 -0500 Subject: [PATCH 37/40] Remove unused markdown files --- CHANGELOG.md | 44 -------------------------------------------- TODO.md | 20 -------------------- 2 files changed, 64 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 TODO.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 18dae9dc..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,44 +0,0 @@ -# Changelog - -## v3.0.0 - -- Added Rigorous CI/CD tools, i.e. `black`, `isort`, `mypy`, `pre-commit`, `pylint`, `flake8`. -- Renamed `AsyncClient` methods with `async_` convention. -- `Client` and `AsyncClient` can be initialized without confirming the API's status. -- `Client` and `AsyncClient` are now both context managers that function the exact same. -- Both clients now share previously redundant model conversion methods. -- Reversed CHANGELOG order (most recent first). - -## v2.4.0.post2 - -- Fixed wrong check in malformed_id function - -## v2.4.0.post1 - -- Replaced `text/plain` with `application/octet-stream` in docs and processing module. -- Added message content to UnrecognizedStatusCodeError to help with user debugging - -## v2.4.0 - -- Bug fixes (see closed issues between releases) -- Added a processing framework for hooking into mimetype processing -- Fixed some issues with some ``AsyncClient`` methods - -## v2.3.0 - -- Bug fixes (see closed issues between releases) -- Added global request kwargs parameter to Client objects (see [docs](https://homeassistantapi.rtfd.io/en/latest/api.html#homeassistant_api.Client)) - -## v2.2.0 - -- Implemented async support with `homeassistant_api._async.AsyncClient` - -## v2.1.0 - -- Added Event support - -## v2.0.0 - -- Added Data Models -- Added Documentation -- Added functions for all endpoints diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 6166afc3..00000000 --- a/TODO.md +++ /dev/null @@ -1,20 +0,0 @@ -# TODOs (A checklist of sorts) - -## Code Features - -- [ ] Add Testing Suite/Workflow the runs Home Assistant Core to test library. -- [X] Clean up Model `repr` methods with disabling model field `repr`s. - -## Code Bugs - -None yet? - -## Maintenance - -- [X] Fix workflows to only run when python paths are modified. - -## Documentation - -- [ ] Document Persistent Caching. -- [ ] Document project scripts. -- [ ] Document branch naming scheme (i.e. `feature/`, `maintenance/`, `bug/`, `docs/`) From 03c630001bf9e8c204561b0fbfe8cbb2b945b38a Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 24 Jun 2022 09:09:05 -0500 Subject: [PATCH 38/40] Fixed extra quote in bash script --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 3705cd44..779c86b3 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -84,7 +84,7 @@ jobs: # Run Test Suite echo "::group::Running Test Suite" - TOKEN = "$(cat test_server/config/token.txt)"" + TOKEN = "$(cat test_server/config/token.txt)" export PYTHONPATH=. export HOMEASSISTANTAPI_URL="http://localhost:8123/api" export HOMEASSISTANTAPI_TOKEN="$TOKEN" From 1a0b724ba58ee3f79dd6e09a37c1ec5bfe2758c4 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 24 Jun 2022 15:33:14 -0500 Subject: [PATCH 39/40] Fixed variable assignation --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/workflows/test_suite.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b05c2608..2687e353 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: "[Add This Feature?]" +title: "[Feature] Blah blah blah" labels: enhancement assignees: GrandMoff100 diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 779c86b3..3aef7116 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -84,7 +84,7 @@ jobs: # Run Test Suite echo "::group::Running Test Suite" - TOKEN = "$(cat test_server/config/token.txt)" + TOKEN="$(cat test_server/config/token.txt)" export PYTHONPATH=. export HOMEASSISTANTAPI_URL="http://localhost:8123/api" export HOMEASSISTANTAPI_TOKEN="$TOKEN" From 83cfd961a756159652b543c9e818545ec658788c Mon Sep 17 00:00:00 2001 From: GrandMoff100 Date: Fri, 24 Jun 2022 20:34:52 +0000 Subject: [PATCH 40/40] [MegaLinter] Apply linters fixes --- .github/workflows/test_suite.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 3aef7116..3a50e2cf 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -72,7 +72,7 @@ jobs: . bin/activate python3 -m pip install homeassistant echo "::endgroup::" - + echo "::group::Starting the Testing Server" hass -c test_server/config & deactivate diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56446f3d..851e4a1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,4 +26,4 @@ repos: rev: "v2.12.2" hooks: - id: pylint - language: system \ No newline at end of file + language: system