feat: add by_type/ registry package with for_device() and DeviceRegistry

This commit is contained in:
Marc Billow committed 2026-06-30 16:32:42 -05:00
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,
]),
)
+65
View File
@@ -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"