diff --git a/src/philipstv/_data.py b/src/philipstv/_data.py index 284a026..a986e9e 100644 --- a/src/philipstv/_data.py +++ b/src/philipstv/_data.py @@ -1,11 +1,10 @@ import json import logging from pathlib import Path -from typing import Any, Dict, Optional, Tuple +from typing import Optional from appdirs import user_data_dir -from pydantic import BaseModel, BaseSettings, ValidationError -from pydantic.env_settings import SettingsSourceCallable +from pydantic import BaseModel, ValidationError _LOGGER = logging.getLogger(__name__) @@ -13,32 +12,27 @@ DATA_FILE = Path(user_data_dir("philipstv", "cyran.dev")) / "data.json" -def json_data_source(settings: BaseSettings) -> Dict[str, Any]: - try: - _LOGGER.debug("Trying to load application data from %s", DATA_FILE) - return json.loads(DATA_FILE.read_text()) # type: ignore [no-any-return] - except FileNotFoundError: - _LOGGER.debug("Data file not found") - return {} - finally: - _LOGGER.debug("Application data loaded successfully") - - class HostData(BaseModel): host: str id: str key: str -class PhilipsTVData(BaseSettings): +class PhilipsTVData(BaseModel): last_host: HostData @classmethod def load(cls) -> Optional["PhilipsTVData"]: try: - return cls() + _LOGGER.debug("Trying to load application data from %s", DATA_FILE) + return cls.model_validate(json.loads(DATA_FILE.read_text())) + except FileNotFoundError: + _LOGGER.debug("Data file not found") + return None except ValidationError: return None + finally: + _LOGGER.debug("Application data loaded successfully") def save(self) -> None: _LOGGER.debug("Saving application data to %s", DATA_FILE) @@ -46,15 +40,5 @@ def save(self) -> None: _LOGGER.debug("Data file doesn't exist, creating") DATA_FILE.parent.mkdir(parents=True, exist_ok=True) DATA_FILE.touch() - DATA_FILE.write_text(self.json()) + DATA_FILE.write_text(self.model_dump_json()) _LOGGER.debug("Application data saved successfully") - - class Config: - @classmethod - def customise_sources( - cls, - init_settings: SettingsSourceCallable, - env_settings: SettingsSourceCallable, - file_secret_settings: SettingsSourceCallable, - ) -> Tuple[SettingsSourceCallable, ...]: - return (init_settings, json_data_source) diff --git a/src/philipstv/model/ambilight.py b/src/philipstv/model/ambilight.py index 9beb0eb..fed2c43 100644 --- a/src/philipstv/model/ambilight.py +++ b/src/philipstv/model/ambilight.py @@ -1,5 +1,6 @@ from typing import Dict, Tuple, Union +from pydantic import RootModel from pydantic.fields import Field from .base import APIObject, StrEnum @@ -92,23 +93,23 @@ class AmbilightLayer(APIObject): """Colors of pixels on the bottom edge.""" -class AmbilightColors(APIObject): +class AmbilightColors(APIObject, RootModel[Dict[str, AmbilightLayer]]): """Model of full Amblight per-pixel color definition. This model is actually a wrapper around a dict since layer names are not predefined. - Values should be set as dict using ``__root__`` kwarg in constructor and accessed as in normal + Values should be set as dict passed to constructor and accessed as in normal dict using indexing:: - colors = AmbilightColors(__root__={"layer1": AmbilightLayer()}) + colors = AmbilightColors({"layer1": AmbilightLayer()}) layer1 = colors["layer1"] """ - __root__: Dict[str, AmbilightLayer] = {} + root: Dict[str, AmbilightLayer] """Mapping of layer name to :class:`AmbilightLayer`.""" def __getitem__(self, item: str) -> AmbilightLayer: - return self.__root__[item] + return self.root[item] AmbilightColorSettings = Union[ diff --git a/src/philipstv/model/base.py b/src/philipstv/model/base.py index 208463d..d72e988 100644 --- a/src/philipstv/model/base.py +++ b/src/philipstv/model/base.py @@ -16,6 +16,8 @@ class APIObject(BaseModel): """ + model_config = {"populate_by_name": True, "use_enum_values": True} + def dump(self) -> Any: """Dump the object's JSON data. @@ -25,8 +27,7 @@ def dump(self) -> Any: Model's JSON data. """ - data = super().dict(by_alias=True) - return data["__root__"] if self.__custom_root_type__ else data + return self.model_dump(by_alias=True) @classmethod def parse(cls: Type[_SelfAPIObject], raw: Any) -> _SelfAPIObject: @@ -45,11 +46,7 @@ def parse(cls: Type[_SelfAPIObject], raw: Any) -> _SelfAPIObject: ValidationError: if given JSON data cannot be parsed into this model. """ - return cls.parse_obj(raw) - - class Config: - allow_population_by_field_name = True - use_enum_values = True + return cls.model_validate(raw) class StrEnum(str, Enum): diff --git a/src/philipstv/remote.py b/src/philipstv/remote.py index 9483ed3..6e23909 100644 --- a/src/philipstv/remote.py +++ b/src/philipstv/remote.py @@ -114,7 +114,7 @@ def pair(self, pin_callback: PinCallback, id: Optional[str] = None) -> Credentia id=id, device_name=uname_info.node, device_os=uname_info.system, - app_id=69, + app_id="69", app_name="philipstv", type="native", ) @@ -273,7 +273,8 @@ def set_ambilight_color( if set_bottom := (bottom or color): sides["bottom"] = self._create_ambilight_side(set_bottom, topology.bottom) - colors = AmbilightColors(__root__={"layer1": AmbilightLayer(**sides)}) + # pydantic.mypy plugin issue: https://github.com/pydantic/pydantic/discussions/7418 + colors = AmbilightColors({"layer1": AmbilightLayer(**sides)}) # type: ignore[misc] self._api.set_ambilight_cached(colors) def _create_ambilight_side( diff --git a/tests/test_cli.py b/tests/test_cli.py index b5b6090..983f745 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -107,9 +107,9 @@ def test_saves_data(data_file: Path, remote: Mock) -> None: run("--host", given_host, "--id", given_id, "--key", given_key, "--save", "power", "get") assert data_file.exists() is True - assert data_file.read_text() == json.dumps( - {"last_host": {"host": given_host, "id": given_id, "key": given_key}} - ) + assert json.loads(data_file.read_text()) == { + "last_host": {"host": given_host, "id": given_id, "key": given_key} + } def test_reads_saved_data(data_file: Path, remote: Mock) -> None: @@ -152,9 +152,9 @@ def test_pair_save(remote: Mock, data_file: Path) -> None: run("--host", given_host, "--id", given_id, "--save", "pair") assert data_file.exists() is True - assert data_file.read_text() == json.dumps( - {"last_host": {"host": given_host, "id": given_id, "key": given_key}} - ) + assert json.loads(data_file.read_text()) == { + "last_host": {"host": given_host, "id": given_id, "key": given_key} + } def test_pair_error(remote: Mock) -> None: diff --git a/tests/test_remote.py b/tests/test_remote.py index 0876707..a03cf78 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -265,8 +265,9 @@ def test_set_ambilight_color_sides(api_mock: Mock) -> None: ) api_mock.set_ambilight_cached.assert_called_once_with( - AmbilightColors( - __root__={ + # pydantic.mypy plugin issue: https://github.com/pydantic/pydantic/discussions/7418 + AmbilightColors( # type: ignore[misc] + { "layer1": AmbilightLayer( left={str(point): left_color for point in range(topology.left)}, top={str(point): top_color for point in range(topology.top)},