feat: add by_type/ registry package with for_device() and DeviceRegistry
This commit is contained in:
1 parent
b10cfad194
commit
e801af9ab7
5 files changed
+185
No files matched your search
@@ -0,0 +1,49 @@
|
||||
"""Per-device-type registries."""
|
||||
from typing import Optional
|
||||
|
||||
from ._base import DeviceRegistry
|
||||
from . import dishwasher, refrigerator
|
||||
|
||||
__all__ = ['DeviceRegistry', '_type_key', 'for_device']
|
||||
|
||||
|
||||
_REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
|
||||
'dishwasher': dishwasher.REGISTRY,
|
||||
'refrigerator': refrigerator.REGISTRY,
|
||||
}
|
||||
|
||||
|
||||
def _type_key(one_ui_version: str) -> str:
|
||||
"""Convert oneUiVersion string to registry key.
|
||||
|
||||
Args:
|
||||
one_ui_version: String like '7.0 Dishwasher' or 'Oven'.
|
||||
|
||||
Returns:
|
||||
Lowercase key with version prefix stripped and spaces/hyphens converted to underscores.
|
||||
|
||||
Examples:
|
||||
'7.0 Dishwasher' -> 'dishwasher'
|
||||
'7.0 French Door Refrigerator' -> 'french_door_refrigerator'
|
||||
'Oven' -> 'oven'
|
||||
"""
|
||||
if ' ' in one_ui_version:
|
||||
# Strip version prefix: everything before and including the first space
|
||||
suffix = one_ui_version.split(' ', 1)[-1]
|
||||
else:
|
||||
suffix = one_ui_version
|
||||
|
||||
return suffix.lower().replace(' ', '_').replace('-', '_')
|
||||
|
||||
|
||||
def for_device(one_ui_version: str) -> Optional[DeviceRegistry]:
|
||||
"""Return the DeviceRegistry for the given oneUiVersion string, or None if unknown.
|
||||
|
||||
Args:
|
||||
one_ui_version: Device's oneUiVersion string (e.g., '7.0 Dishwasher').
|
||||
|
||||
Returns:
|
||||
DeviceRegistry if a matching registry exists, None otherwise.
|
||||
"""
|
||||
key = _type_key(one_ui_version)
|
||||
return _REGISTRY_BY_KEY.get(key)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Base DeviceRegistry dataclass and builder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from ..capability import Capability
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceRegistry:
|
||||
"""Registry of capabilities for a specific device type."""
|
||||
name: str
|
||||
capabilities: dict[str, list[Capability]]
|
||||
pattern_capabilities: list[Capability] = field(default_factory=list)
|
||||
|
||||
|
||||
def _build(caps: list[Capability]) -> dict[str, list[Capability]]:
|
||||
"""Build a capabilities dict from a list of Capability objects.
|
||||
|
||||
Args:
|
||||
caps: List of Capability objects to organize by href.
|
||||
|
||||
Returns:
|
||||
A dict mapping href to list of Capability objects sharing that href.
|
||||
|
||||
Raises:
|
||||
ValueError: If any capability has href=None (use pattern_capabilities instead),
|
||||
or if multiple caps share an href without all having rt_filter or match_fn.
|
||||
"""
|
||||
out: dict[str, list[Capability]] = {}
|
||||
|
||||
for cap in caps:
|
||||
if cap.href is None:
|
||||
raise ValueError(f"Use pattern_capabilities for href=None caps")
|
||||
out.setdefault(cap.href, []).append(cap)
|
||||
|
||||
# Validate that multi-cap hrefs have proper discrimination
|
||||
for href, cs in out.items():
|
||||
if len(cs) > 1 and any(c.rt_filter is None and c.match_fn is None for c in cs):
|
||||
raise ValueError(
|
||||
f"href {href!r} has multiple caps but at least one lacks rt_filter and match_fn")
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Dishwasher device registry."""
|
||||
from ..capabilities import common, operational
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
# Placeholder: Task 4 will expand with full capability list.
|
||||
# For now, include common capabilities to verify the import chain.
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='dishwasher',
|
||||
capabilities=_build([
|
||||
common.POWER,
|
||||
common.KIDS_LOCK,
|
||||
common.REMOTE_CONTROL,
|
||||
]),
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Refrigerator device registry."""
|
||||
from ..capabilities import common, fridge
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
# Placeholder: Task 4 will expand with full capability list.
|
||||
# For now, include common capabilities to verify the import chain.
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='refrigerator',
|
||||
capabilities=_build([
|
||||
common.POWER,
|
||||
common.KIDS_LOCK,
|
||||
common.REMOTE_CONTROL,
|
||||
]),
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Tests for samsung_appliance/registry/by_type."""
|
||||
import pytest
|
||||
from samsung_appliance.registry.by_type import _type_key, for_device, DeviceRegistry
|
||||
|
||||
|
||||
class TestTypeKey:
|
||||
"""Tests for _type_key() function."""
|
||||
|
||||
def test_type_key_strips_version_prefix(self):
|
||||
"""'7.0 Dishwasher' -> 'dishwasher'"""
|
||||
assert _type_key("7.0 Dishwasher") == "dishwasher"
|
||||
|
||||
def test_type_key_preserves_spaces_as_underscores(self):
|
||||
"""'7.0 French Door Refrigerator' -> 'french_door_refrigerator'"""
|
||||
assert _type_key("7.0 French Door Refrigerator") == "french_door_refrigerator"
|
||||
|
||||
def test_type_key_no_space_returns_lowercase(self):
|
||||
"""'Oven' -> 'oven' (no space in string)"""
|
||||
assert _type_key("Oven") == "oven"
|
||||
|
||||
|
||||
class TestForDevice:
|
||||
"""Tests for for_device() function."""
|
||||
|
||||
def test_for_device_returns_dishwasher_registry(self):
|
||||
"""for_device("7.0 Dishwasher") returns a non-None DeviceRegistry."""
|
||||
registry = for_device("7.0 Dishwasher")
|
||||
assert registry is not None
|
||||
assert isinstance(registry, DeviceRegistry)
|
||||
assert registry.name == "dishwasher"
|
||||
|
||||
def test_for_device_unknown_returns_none(self):
|
||||
"""for_device("7.0 Toaster") returns None for unknown device type."""
|
||||
registry = for_device("7.0 Toaster")
|
||||
assert registry is None
|
||||
|
||||
|
||||
class TestDeviceRegistries:
|
||||
"""Tests for device registries themselves."""
|
||||
|
||||
def test_dishwasher_registry_has_no_dup_hrefs(self):
|
||||
"""All caps in dishwasher registry have unique hrefs (or meet disambiguation rule)."""
|
||||
registry = for_device("7.0 Dishwasher")
|
||||
assert registry is not None
|
||||
|
||||
# Each href should map to exactly one cap (or multiple with rt_filter/match_fn)
|
||||
for href, caps in registry.capabilities.items():
|
||||
if len(caps) > 1:
|
||||
# If multiple caps share an href, all must have rt_filter or match_fn
|
||||
for cap in caps:
|
||||
assert cap.rt_filter is not None or cap.match_fn is not None, \
|
||||
f"href {href!r} has multiple caps but {cap!r} lacks rt_filter and match_fn"
|
||||
|
||||
def test_refrigerator_registry_has_no_dup_hrefs(self):
|
||||
"""All caps in refrigerator registry have unique hrefs (or meet disambiguation rule)."""
|
||||
registry = for_device("7.0 Refrigerator")
|
||||
assert registry is not None
|
||||
|
||||
# Each href should map to exactly one cap (or multiple with rt_filter/match_fn)
|
||||
for href, caps in registry.capabilities.items():
|
||||
if len(caps) > 1:
|
||||
# If multiple caps share an href, all must have rt_filter or match_fn
|
||||
for cap in caps:
|
||||
assert cap.rt_filter is not None or cap.match_fn is not None, \
|
||||
f"href {href!r} has multiple caps but {cap!r} lacks rt_filter and match_fn"
|
||||
Reference in new issue
Block a user