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/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/mega-linter.yml b/.github/workflows/mega-linter.yml index 99e79f0b..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 diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml new file mode 100644 index 00000000..3a50e2cf --- /dev/null +++ b/.github/workflows/test_suite.yml @@ -0,0 +1,95 @@ +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: + schedule: + - cron: 0 12 * * 6 + +jobs: + code_styling: + name: "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: + name: "Code Functionality" + runs-on: ubuntu-latest + steps: + - 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: | + sudo apt-get -qq install -y libffi-dev \ + libssl-dev libjpeg-dev zlib1g-dev \ + autoconf build-essential libopenjp2-7 libtiff5 \ + libturbojpeg0-dev tzdata + - name: Unpack Testing Server Config + run: | + unzip tests/homeassistant.zip + - name: Initialize Testing Server and Run Suite + run: | + echo "::group::Install Project Dependencies" + python3 -m pip install poetry + python3 -m poetry install + echo "::endgroup::" + + echo "::group::Install Home Assistant Dependencies" + python3 -m venv . + . bin/activate + python3 -m pip install homeassistant + echo "::endgroup::" + + 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 "::group::Running Test Suite" + TOKEN="$(cat test_server/config/token.txt)" + export PYTHONPATH=. + export HOMEASSISTANTAPI_URL="http://localhost:8123/api" + export HOMEASSISTANTAPI_TOKEN="$TOKEN" + poetry run pytest tests --disable-warnings + echo "::endgroup::" + env: + HOMEASSISTANTAPI_URL: ${{ env.HOMEASSISTANTAPI_URL }} + HOMEASSISTANTAPI_TOKEN: ${{ env.HOMEASSISTANTAPI_TOKEN }} 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/ 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 471c573a..6163fb5e 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 @@ -11,4 +12,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 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/`) 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/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/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/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/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/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..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): @@ -95,3 +93,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..75f45fa7 100644 --- a/homeassistant_api/models/events.py +++ b/homeassistant_api/models/events.py @@ -18,11 +18,16 @@ 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) 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, **event_data) return cast(Dict[str, str], data).get("message", "No message provided") 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 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 1c258483..74ea3d4f 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""" @@ -26,22 +21,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 += "/" @@ -107,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, @@ -153,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 diff --git a/homeassistant_api/_async/asyncclient.py b/homeassistant_api/rawasyncclient.py similarity index 74% rename from homeassistant_api/_async/asyncclient.py rename to homeassistant_api/rawasyncclient.py index 66257675..381915f4 100644 --- a/homeassistant_api/_async/asyncclient.py +++ b/homeassistant_api/rawasyncclient.py @@ -3,17 +3,33 @@ import logging from datetime import datetime from posixpath import join -from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union, cast +from typing import ( + Any, + AsyncGenerator, + Dict, + List, + Literal, + 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 +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__) @@ -27,19 +43,30 @@ class RawAsyncClient(RawWrapper, JsonProcessingMixin): :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 + 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): - self._async_session = CachedSession( - expire_after=self.cache_expire_after, cache=self.cache_backend + logger.debug( + "Entering cached async requests session %r", self.async_cache_session ) - logger.debug(f"Entering cached requests session {self._async_session!r}") - await self._async_session.__aenter__() + await self.async_cache_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) + logger.debug("Exiting async requests session %r", self.async_cache_session) + await self.async_cache_session.__aexit__(cls, obj, traceback) # Very important request function async def async_request( @@ -53,9 +80,9 @@ async def async_request( try: if self.global_request_kwargs is not None: kwargs.update(self.global_request_kwargs) - if self._async_session is not None: + if self.async_cache_session: return await self.async_response_logic( - await self._async_session.request( + await self.async_cache_session.request( method, self.endpoint(path), headers=self.prepare_headers(headers), @@ -80,15 +107,15 @@ async def async_response_logic(response) -> Any: return await Processing(response=response).process() # API information methods - async def async_api_error_log(self) -> str: + 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_api_config(self) -> Dict[str, Any]: + 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_logbook_entries( + async def async_get_logbook_entries( self, *args, **kwargs, @@ -101,7 +128,7 @@ async def async_logbook_entries( async def async_get_entity_histories( self, - entities: Optional[Tuple[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, @@ -127,16 +154,24 @@ async def async_get_entity_histories( 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", - ) + 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]: + @staticmethod + async def async_get_discovery_info() -> 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")) + 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: @@ -162,13 +197,13 @@ async def async_check_api_running(self) -> bool: raise MalformedDataError("Server response did not return message attribute") # Entity methods - async def async_get_entities(self) -> Tuple[AsyncGroup, ...]: + async def async_get_entities(self) -> Tuple[Group, ...]: """Fetches all entities from the api""" - entities: Dict[str, AsyncGroup] = {} + 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] = AsyncGroup(group_id=group_id, client=self) + entities[group_id] = Group(group_id=group_id, client=self) entities[group_id].add_entity(entity_slug, state) return tuple(entities.values()) @@ -177,7 +212,7 @@ async def async_get_entity( group_id: str = None, entity_slug: str = None, entity_id: str = None, - ) -> Optional[AsyncEntity]: + ) -> 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) @@ -192,44 +227,38 @@ async def async_get_entity( 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 = 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) -> Tuple[Domain, ...]: - """Fetches all Services from the api""" + async def async_get_domains(self) -> Dict[str, Domain]: + """Fetches all services from the api""" data = await self.async_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} async def async_get_domain(self, domain_id: str) -> Optional[Domain]: - """Fetchers all services under a particular domain.""" + """Fetches 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 + return domains.get(domain_id) async def async_trigger_service( self, domain: str, service: str, **service_data: Union[Dict[str, Any], List[Any], str], - ) -> List[State]: + ) -> 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 [ - self.process_state_json(state_data) - for state_data in cast(List[Dict[Any, Any]], 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 @@ -281,14 +310,15 @@ async def async_get_states(self) -> List[State]: # Event methods async def async_get_events(self) -> Tuple[Event, ...]: - """Gets the Events that happen within homeassistant""" + """Gets the internal 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), + if isinstance(data, list): + return tuple( + map( + self.process_event_json, + cast(List[Dict[str, 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: @@ -303,3 +333,8 @@ async def async_fire_event(self, event_type: str, **event_data) -> str: 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.""" + 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 268291af..c87266a6 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 @@ -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("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._session!r}") - self._session.__exit__() - self._session = None + logger.debug("Exiting requests session %r", self.cache_session) + self.cache_session.__exit__(*args) def request( self, @@ -55,9 +61,9 @@ 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)}") - if self._session is not None: - resp = self._session.request( + logger.debug("%s request to %s", method, self.endpoint(path)) + 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, @@ -130,26 +136,33 @@ 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]: + @staticmethod + def get_discovery_info() -> 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: @@ -157,7 +170,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 @@ -199,22 +212,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, @@ -222,14 +231,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 @@ -259,7 +267,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, @@ -284,17 +292,24 @@ 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", "No message provided") + return cast(dict, data).get("message") + + def get_components(self) -> Tuple[str, ...]: + """Returns a tuple of all registered components.""" + return tuple(self.request("components")) diff --git a/poetry.lock b/poetry.lock index 4273ba4c..add4e4ac 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" @@ -128,7 +142,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[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 = "*" @@ -541,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 @@ -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 = "03408a135a7895dbf85044f0798fd65a97bc98bb8b61b7d7a2dc065cff01f7e3" [metadata.files] aiodns = [ @@ -1126,13 +1174,17 @@ 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"}, @@ -1142,12 +1194,12 @@ attrs = [ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] 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"}, @@ -1659,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"}, @@ -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..115f6b0c 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,14 +19,17 @@ 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" 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" + [tool.poetry.dev-dependencies] black = "^22.3.0" flake8 = "^4.0.1" @@ -42,12 +46,18 @@ 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" -[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/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.zip b/tests/homeassistant.zip new file mode 100644 index 00000000..bb298228 Binary files /dev/null and b/tests/homeassistant.zip differ diff --git a/tests/server_config.yaml b/tests/server_config.yaml new file mode 100644 index 00000000..18960898 --- /dev/null +++ b/tests/server_config.yaml @@ -0,0 +1,24 @@ +# Gets renamed to configuration.yaml in the testing volume + +person: +sun: +api: +timer: +input_number: +input_boolean: +input_datetime: +input_text: +logbook: +history: +trace: +counter: +tag: +notify: + +logger: + default: info + +http: + trusted_proxies: + - 127.0.0.1 + use_x_forwarded_for: true diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py new file mode 100644 index 00000000..edcad46d --- /dev/null +++ b/tests/test_endpoints.py @@ -0,0 +1,246 @@ +# pylint: disable=redefined-outer-name +import os +from typing import AsyncGenerator, 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() -> AsyncGenerator[Client, 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.""" + 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.""" + 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) + + +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 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", + ) + 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") + 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)", + ) + 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) + + +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." + + +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."