Skip to content

Commit

Permalink
feat: add model foundation (#24)
Browse files Browse the repository at this point in the history
* feat: add model and client methods

* fix: replace number with float

* test: fix test

* fix: update model with production swagger

* fix: adjust headers

* fix: handle optional items

* fix: update model

* feat: provide base for model keys

* chore: add more setting keys

* chore: complete setting keys

* chore: use enums in model key attributes

* chore: clean client interface

* chore: move model to package

* fix: update cli call
  • Loading branch information
MartinHjelmare authored Nov 24, 2024
1 parent 2f8b32d commit 722f614
Show file tree
Hide file tree
Showing 15 changed files with 1,612 additions and 42 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,4 @@ token.json

# Swagger YAML
hcsdk.yaml
hcsdk-production.yaml
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ ignore = [
"EM101", # raw-string-in-exception
"EM102", # f-string-in-exception
"ISC001", # single-line-implicit-string-concatenation
"PLR0913", # too-many-arguments
"PLR2004", # magic-value-comparison
"TC001", # typing-only-first-party-import
"TC002", # typing-only-third-party-import
Expand Down
101 changes: 72 additions & 29 deletions scripts/swagger_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
("ArrayOfHomeAppliances", "homeappliances"): "HomeAppliance",
("ArrayOfEvents", "items"): "Event",
("Program", "options"): "Option",
("Program", "constraints"): "ProgramConstraints",
("ArrayOfAvailablePrograms", "programs"): "EnumerateAvailableProgram",
(
"EnumerateAvailableProgram",
"constraints",
): "EnumerateAvailableProgramConstraints",
("EnumerateAvailableProgramConstraints", "execution"): "Execution",
("ArrayOfPrograms", "programs"): "EnumerateProgram",
("ArrayOfPrograms", "active"): "Program",
("ArrayOfPrograms", "selected"): "Program",
("EnumerateProgram", "constraints"): "EnumerateProgramConstraints",
("EnumerateProgramConstraints", "execution"): "Execution",
("ProgramDefinition", "options"): "ProgramDefinitionOption",
Expand All @@ -43,6 +46,8 @@
("PutSettings", "data"): "PutSetting",
("ArrayOfStatus", "status"): "Status",
("Status", "constraints"): "StatusConstraints",
("ArrayOfCommands", "commands"): "Command",
("PutCommands", "data"): "PutCommand",
}
PARAMETER_ENUM_MAP = {
"Accept": "ContentType",
Expand Down Expand Up @@ -171,9 +176,12 @@ class DefinitionModelBase(ABC):
definition: str
all_definitions: list[str]
description: str | None = None
generated_classes: set[str] = field(default_factory=set)

@abstractmethod
def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""

@staticmethod
Expand All @@ -192,18 +200,24 @@ class {definition}(DataClassJSONMixin):
class DefinitionModelUnknown(DefinitionModelBase):
"""Represent a string type model."""

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
return "Any"
suffix = "" if required else " | None"
return f"Any{suffix}"


@dataclass(kw_only=True)
class DefinitionModelString(DefinitionModelBase):
"""Represent a string type model."""

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
return "str"
suffix = "" if required else " | None"
return f"str{suffix}"


@dataclass(kw_only=True)
Expand All @@ -212,16 +226,18 @@ class DefinitionModelStringEnum(DefinitionModelBase):

enum: list[str]

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
if (description := self.description) is None:
raise ValueError("Missing description")
suffix = ""
suffix = "" if required else " | None"
if generate_class:
code_enum = "\n ".join(
f'{enum.upper()} = "{enum}"' for enum in self.enum
)
suffix = (
self.generated_classes.add(
"\n\n"
f"class {definition.capitalize()}(StrEnum):\n"
f' """{description.strip()}."""\n\n '
Expand All @@ -237,27 +253,36 @@ class DefinitionModelInteger(DefinitionModelBase):

format_: str | None = None

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
return "int"
suffix = "" if required else " | None"
return f"int{suffix}"


@dataclass(kw_only=True)
class DefinitionModelBoolean(DefinitionModelBase):
"""Represent a boolean type model."""

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
return "bool"
suffix = "" if required else " | None"
return f"bool{suffix}"


@dataclass(kw_only=True)
class DefinitionModelStringNumberBoolean(DefinitionModelBase):
"""Represent a union of string number and boolean type model."""

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
return "str | Number | bool"
suffix = "" if required else " | None"
return f"str | float | bool{suffix}"


@dataclass(kw_only=True)
Expand All @@ -275,20 +300,25 @@ def __post_init__(self) -> None:
data=self.raw_items,
)

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
suffix = ""
suffix = "" if required else " | None"
if definition != self.definition and generate_class:
item = definition
if definition not in self.all_definitions:
suffix = (
self.generated_classes.add(
"\n\n"
f"{self.generate_class(definition)} "
f"{self.items.generate_code(definition)}"
"\n\n"
)
else:
item = self.items.generate_code(definition)

self.generated_classes.update(self.items.generated_classes)
self.items.generated_classes.clear()
return f"list[{item}]{suffix}"


Expand All @@ -312,12 +342,14 @@ def __post_init__(self) -> None:

self.properties = properties

def generate_code(self, definition: str, *, generate_class: bool = False) -> str:
def generate_code(
self, definition: str, *, generate_class: bool = False, required: bool = False
) -> str:
"""Return the Python code as a string for this model."""
if definition != self.definition and generate_class:
suffix = ""
suffix = "" if required else " | None"
if definition not in self.all_definitions:
suffix = (
self.generated_classes.add(
"\n\n"
f"{self.generate_class(definition)} "
f"{self.generate_code(definition)}"
Expand All @@ -326,32 +358,44 @@ def generate_code(self, definition: str, *, generate_class: bool = False) -> str
return f"{definition}{suffix}"
properties = ""
sorted_properties = {}
if required := self.required:
for prop in required:
if required_properties := self.required:
for prop in required_properties:
sorted_properties[prop] = self.properties[prop]
sorted_properties.update(self.properties)

generate_class = False
original_definition = definition
for prop, model in self.properties.items():
required_property = bool(
required_properties and prop in required_properties
)
if nested_definition := DEFINITION_NESTED_MAP.get((definition, prop)):
definition = nested_definition
generate_class = True
if prop in ("data", "error") and definition == self.definition:
properties += f" {model.generate_code(definition)}\n"
model_code = model.generate_code(definition, required=required_property)
properties += f" {model_code}\n"
else:
prop_code = model.generate_code(
definition, generate_class=generate_class
definition,
generate_class=generate_class,
required=required_property,
)
properties += f" {prop}: {prop_code}\n"
definition = original_definition

return f"{properties}".strip()
self.generated_classes.update(model.generated_classes)
model.generated_classes.clear()
suffix = (
"".join(self.generated_classes)
if self.definition == original_definition
else ""
)
return f"{properties}{suffix}".strip()


def load_yaml() -> dict[str, Any]:
def load_yaml(path: str = "hcsdk-production.yaml") -> dict[str, Any]:
"""Load yaml."""
raw = Path("hcsdk.yaml").read_text(encoding="utf-8")
raw = Path(path).read_text(encoding="utf-8")
return load(raw, Loader=SafeLoader)


Expand Down Expand Up @@ -445,7 +489,6 @@ def run() -> None:
from dataclasses import dataclass
from enum import StrEnum
from numbers import Number
from typing import Any
from mashumaro.mixins.json import DataClassJSONMixin
Expand Down
10 changes: 8 additions & 2 deletions src/aiohomeconnect/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import typer
import uvicorn

from aiohomeconnect.model import StatusKey

from .client import CLIClient, TokenManager

cli = typer.Typer()
Expand Down Expand Up @@ -74,7 +76,7 @@ async def _get_appliances(
) -> None:
"""Get the appliances."""
client = CLIClient(client_id, client_secret)
rich_print(await client.get_appliances())
rich_print(await client.get_home_appliances())


@cli.command()
Expand All @@ -86,7 +88,11 @@ def get_operation_state(client_id: str, client_secret: str, ha_id: str) -> None:
async def _get_operation_state(client_id: str, client_secret: str, ha_id: str) -> None:
"""Get the operation state of the device."""
client = CLIClient(client_id, client_secret)
rich_print(await client.get_operation_state(ha_id))
rich_print(
await client.get_status_value(
ha_id, status_key=StatusKey.BSH_COMMON_OPERATION_STATE
)
)


if __name__ == "__main__":
Expand Down
Loading

0 comments on commit 722f614

Please sign in to comment.