Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions pyoverkiz/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@
)
from backoff.types import Details

from pyoverkiz._case import decamelize
from pyoverkiz.action_queue import ActionQueue, ActionQueueSettings
from pyoverkiz.auth import AuthStrategy, Credentials, build_auth_strategy
from pyoverkiz.const import SUPPORTED_SERVERS, USER_AGENT
from pyoverkiz.converter import converter
from pyoverkiz.converter import converter, structure_response
from pyoverkiz.enums import APIType, ExecutionMode, Protocol, Server
from pyoverkiz.exceptions import (
ExecutionQueueFullError,
Expand Down Expand Up @@ -325,7 +324,7 @@ async def get_setup(self, refresh: bool = False) -> Setup:

response = await self._get("setup")

setup = converter.structure(decamelize(response), Setup)
setup = structure_response(response, Setup)

# Cache response
self.setup = setup
Expand Down Expand Up @@ -373,7 +372,7 @@ async def get_devices(self, refresh: bool = False) -> list[Device]:
return self.devices

response = await self._get("setup/devices")
devices = converter.structure(decamelize(response), list[Device])
devices = structure_response(response, list[Device])

# Cache response
self.devices = devices
Expand All @@ -392,7 +391,7 @@ async def get_gateways(self, refresh: bool = False) -> list[Gateway]:
return self.gateways

response = await self._get("setup/gateways")
gateways = converter.structure(decamelize(response), list[Gateway])
gateways = structure_response(response, list[Gateway])

# Cache response
self.gateways = gateways
Expand All @@ -405,7 +404,7 @@ async def get_gateways(self, refresh: bool = False) -> list[Gateway]:
async def get_execution_history(self) -> list[HistoryExecution]:
"""List past executions and their outcomes."""
response = await self._get("history/executions")
return converter.structure(decamelize(response), list[HistoryExecution])
return structure_response(response, list[HistoryExecution])

@retry_on_auth_error
async def get_device_definition(self, deviceurl: str) -> dict[str, Any] | None:
Expand All @@ -422,7 +421,7 @@ async def get_state(self, deviceurl: str) -> list[State]:
response = await self._get(
f"setup/devices/{urllib.parse.quote_plus(deviceurl)}/states"
)
return converter.structure(decamelize(response), list[State])
return structure_response(response, list[State])

@retry_on_auth_error
async def refresh_states(self) -> None:
Expand Down Expand Up @@ -465,7 +464,7 @@ async def fetch_events(self) -> list[Event]:
operation (polling).
"""
response = await self._post(f"events/{self.event_listener_id}/fetch")
return converter.structure(decamelize(response), list[Event])
return structure_response(response, list[Event])

async def unregister_event_listener(self) -> None:
"""Unregister an event listener.
Expand All @@ -485,13 +484,13 @@ async def get_current_execution(self, exec_id: str) -> Execution | None:
if not response or not isinstance(response, dict):
return None

return converter.structure(decamelize(response), Execution)
return structure_response(response, Execution)

@retry_on_auth_error
async def get_current_executions(self) -> list[Execution]:
"""Get all currently running executions."""
response = await self._get("exec/current")
return converter.structure(decamelize(response), list[Execution])
return structure_response(response, list[Execution])

@retry_on_auth_error
async def get_api_version(self) -> str:
Expand Down Expand Up @@ -629,7 +628,7 @@ async def cancel_execution(self, exec_id: str) -> None:
async def get_action_groups(self) -> list[PersistedActionGroup]:
"""List action groups persisted on the server."""
response = await self._get("actionGroups")
return converter.structure(decamelize(response), list[PersistedActionGroup])
return structure_response(response, list[PersistedActionGroup])

@retry_on_auth_error
async def get_places(self) -> Place:
Expand All @@ -644,7 +643,7 @@ async def get_places(self) -> Place:
- `sub_places`: List of nested places within this location
"""
response = await self._get("setup/places")
return converter.structure(decamelize(response), Place)
return structure_response(response, Place)

@retry_on_auth_error
async def execute_persisted_action_group(self, oid: str) -> str:
Expand All @@ -666,7 +665,7 @@ async def get_setup_options(self) -> list[Option]:
Access scope : Full enduser API access (enduser/*).
"""
response = await self._get("setup/options")
return converter.structure(decamelize(response), list[Option])
return structure_response(response, list[Option])

@retry_on_auth_error
async def get_setup_option(self, option: str) -> Option | None:
Expand All @@ -677,7 +676,7 @@ async def get_setup_option(self, option: str) -> Option | None:
response = await self._get(f"setup/options/{option}")

if response:
return converter.structure(decamelize(response), Option)
return structure_response(response, Option)

return None

Expand All @@ -695,7 +694,7 @@ async def get_setup_option_parameter(
response = await self._get(f"setup/options/{option}/{parameter}")

if response:
return converter.structure(decamelize(response), OptionParameter)
return structure_response(response, OptionParameter)

return None

Expand Down Expand Up @@ -762,7 +761,7 @@ async def get_reference_ui_profile(self, profile_name: str) -> UIProfileDefiniti
response = await self._get(
f"reference/ui/profile/{urllib.parse.quote_plus(profile_name)}"
)
return converter.structure(decamelize(response), UIProfileDefinition)
return structure_response(response, UIProfileDefinition)

@retry_on_auth_error
async def get_reference_ui_profile_names(self) -> list[str]:
Expand All @@ -778,7 +777,7 @@ async def get_reference_ui_widgets(self) -> list[str]:
async def get_devices_not_up_to_date(self) -> list[Device]:
"""Get all devices whose firmware is not up to date."""
response = await self._get("setup/devices/notUpToDate")
return converter.structure(decamelize(response), list[Device])
return structure_response(response, list[Device])

@retry_on_auth_error
async def get_device_firmware_status(self, deviceurl: str) -> FirmwareStatus | None:
Expand All @@ -792,7 +791,7 @@ async def get_device_firmware_status(self, deviceurl: str) -> FirmwareStatus | N
)
except UnsupportedOperationError:
return None
return converter.structure(decamelize(response), FirmwareStatus)
return structure_response(response, FirmwareStatus)

@retry_on_auth_error
async def get_device_firmware_update_capability(self, deviceurl: str) -> bool:
Expand Down
6 changes: 6 additions & 0 deletions pyoverkiz/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import attr
import cattrs

from pyoverkiz._case import decamelize
from pyoverkiz.models import (
CommandDefinition,
CommandDefinitions,
Expand Down Expand Up @@ -65,3 +66,8 @@ def _structure_command_definitions(val: Any, _: type) -> CommandDefinitions:


converter = _make_converter()


def structure_response[T](data: Any, cls: type[T]) -> T:
Comment thread
iMicknl marked this conversation as resolved.
"""Decamelize an API response and structure it into the target type."""
return converter.structure(decamelize(data), cls)
35 changes: 17 additions & 18 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import cattrs.errors
import pytest

from pyoverkiz._case import decamelize
from pyoverkiz.converter import converter
from pyoverkiz.converter import converter, structure_response
from pyoverkiz.enums import DataType, EventName, ExecutionState, FailureType, Protocol
from pyoverkiz.models import (
Action,
Expand Down Expand Up @@ -88,7 +87,7 @@

def _make_device(raw: dict | None = None) -> Device:
"""Create a Device from raw camelCase dict via the converter."""
return converter.structure(decamelize(raw or RAW_DEVICES), Device)
return structure_response(raw or RAW_DEVICES, Device)


class TestSetup:
Expand All @@ -99,7 +98,7 @@ def test_id_is_raw_but_repr_is_redacted_when_present(self):
raw_setup = json.loads(
(FIXTURES_DIR / "setup_tahoma_1.json").read_text(encoding="utf-8")
)
setup = converter.structure(decamelize(raw_setup), Setup)
setup = structure_response(raw_setup, Setup)
raw_id = "SETUP-1234-1234-8044"
redacted_id = obfuscate_id(raw_id)

Expand All @@ -112,7 +111,7 @@ def test_id_is_none_when_missing(self):
raw_setup = json.loads(
(FIXTURES_DIR / "setup_local.json").read_text(encoding="utf-8")
)
setup = converter.structure(decamelize(raw_setup), Setup)
setup = structure_response(raw_setup, Setup)

assert setup.id is None

Expand Down Expand Up @@ -838,7 +837,7 @@ class TestEvent:

def test_execution_state_changed_event(self):
"""Optional[Enum] fields (old_state, new_state) are structured into enums."""
raw = decamelize(
event = structure_response(
{
"timestamp": 1631130760744,
"setupOID": "741bc89f-a47b-4ad6-894d-a785c06956c2",
Expand All @@ -850,9 +849,9 @@ def test_execution_state_changed_event(self):
"oldState": "TRANSMITTED",
"timeToNextState": 0,
"name": "ExecutionStateChangedEvent",
}
},
Event,
)
event = converter.structure(raw, Event)

assert event.name == EventName.EXECUTION_STATE_CHANGED
assert event.old_state is ExecutionState.TRANSMITTED
Expand All @@ -861,36 +860,36 @@ def test_execution_state_changed_event(self):

def test_failure_type_code_structured_as_enum(self):
"""FailureType | None field is structured into an enum instance."""
raw = decamelize(
event = structure_response(
{
"name": "ExecutionStateChangedEvent",
"timestamp": 123,
"failureTypeCode": 0,
}
},
Event,
)
event = converter.structure(raw, Event)

assert isinstance(event.failure_type_code, FailureType)
assert event.failure_type_code is FailureType.NO_FAILURE

def test_optional_enum_fields_none_when_absent(self):
"""Optional enum fields default to None when not present in the payload."""
raw = decamelize(
event = structure_response(
{
"name": "GatewaySynchronizationEndedEvent",
"timestamp": 1631130645998,
"gatewayId": "9876-1234-8767",
}
},
Event,
)
event = converter.structure(raw, Event)

assert event.old_state is None
assert event.new_state is None
assert event.failure_type_code is None

def test_device_state_changed_event_with_states(self):
"""DeviceStateChangedEvent payload structures device_states as EventState."""
raw = decamelize(
event = structure_response(
{
"timestamp": 1631130646544,
"setupOID": "741bc89f-a47b-4ad6-894d-a785c06956c2",
Expand All @@ -903,9 +902,9 @@ def test_device_state_changed_event_with_states(self):
}
],
"name": "DeviceStateChangedEvent",
}
},
Event,
)
event = converter.structure(raw, Event)

assert event.name == EventName.DEVICE_STATE_CHANGED
assert len(event.device_states) == 1
Expand All @@ -914,7 +913,7 @@ def test_device_state_changed_event_with_states(self):
def test_event_fixture_structures_all_events(self):
"""All events in the cloud fixture file structure without errors."""
raw_events = json.loads((Path("tests/fixtures/event/events.json")).read_text())
events = [converter.structure(decamelize(e), Event) for e in raw_events]
events = [structure_response(e, Event) for e in raw_events]

assert len(events) == len(raw_events)
state_changed = [
Expand Down