Coverage for src / bluetooth_sig / registry / profiles / permitted_characteristics.py: 85%
66 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Permitted Characteristics Registry for profile service constraints."""
3from __future__ import annotations
5from pathlib import Path
6from typing import Any, cast
8import msgspec
10from bluetooth_sig.registry.base import BaseGenericRegistry
11from bluetooth_sig.registry.utils import find_bluetooth_sig_path
12from bluetooth_sig.types.registry.profile_types import PermittedCharacteristicEntry
14# Profile subdirectories that contain ``*_permitted_characteristics.yaml``.
15_PROFILE_DIRS: tuple[str, ...] = ("ess", "uds", "imds")
18class PermittedCharacteristicsRegistry(
19 BaseGenericRegistry["PermittedCharacteristicsRegistry"],
20):
21 """Registry for profile-specific permitted characteristic lists.
23 Loads ``permitted_characteristics`` YAML files from ESS, UDS and IMDS
24 profile subdirectories under ``profiles_and_services/``.
26 Thread-safe: Multiple threads can safely access the registry concurrently.
27 """
29 def __init__(self) -> None:
30 """Initialise the permitted characteristics registry."""
31 super().__init__()
32 self._entries: dict[str, list[PermittedCharacteristicEntry]] = {}
34 # ------------------------------------------------------------------
35 # Loading
36 # ------------------------------------------------------------------
38 def _load_yaml_file(self, yaml_path: Path, profile: str) -> None:
39 """Load a single permitted-characteristics YAML file."""
40 if not yaml_path.exists():
41 return
43 with yaml_path.open("r", encoding="utf-8") as fh:
44 data = msgspec.yaml.decode(fh.read())
46 if not isinstance(data, dict):
47 return
49 data_dict = cast("dict[str, Any]", data)
50 items_raw = data_dict.get("permitted_characteristics")
51 if not isinstance(items_raw, list):
52 return
54 entries: list[PermittedCharacteristicEntry] = []
55 for item in items_raw:
56 if not isinstance(item, dict):
57 continue
58 service = item.get("service")
59 chars_raw = item.get("characteristics")
60 if not isinstance(service, str) or not isinstance(chars_raw, list):
61 continue
62 characteristics = tuple(str(c) for c in chars_raw if isinstance(c, str))
63 if characteristics:
64 entries.append(
65 PermittedCharacteristicEntry(
66 service=service,
67 characteristics=characteristics,
68 ),
69 )
71 if entries:
72 self._entries[profile] = entries
74 def _load(self) -> None:
75 """Load all permitted-characteristics YAML files."""
76 uuids_path = find_bluetooth_sig_path()
77 if not uuids_path:
78 self._loaded = True
79 return
81 profiles_path = uuids_path.parent / "profiles_and_services"
82 if not profiles_path.exists():
83 self._loaded = True
84 return
86 for profile_dir in _PROFILE_DIRS:
87 dir_path = profiles_path / profile_dir
88 if not dir_path.is_dir():
89 continue
90 for yaml_file in sorted(dir_path.glob("*_permitted_characteristics.yaml")):
91 self._load_yaml_file(yaml_file, profile_dir)
93 self._loaded = True
95 # ------------------------------------------------------------------
96 # Query API
97 # ------------------------------------------------------------------
99 def get_permitted_characteristics(self, profile: str) -> list[str]:
100 """Get the flat list of permitted characteristic identifiers for a profile.
102 Args:
103 profile: Profile key (e.g. ``"ess"``, ``"uds"``, ``"imds"``).
105 Returns:
106 List of characteristic identifier strings, or an empty list.
107 """
108 self._ensure_loaded()
109 with self._lock:
110 entries = self._entries.get(profile, [])
111 return [c for entry in entries for c in entry.characteristics]
113 def get_entries(self, profile: str) -> list[PermittedCharacteristicEntry]:
114 """Get the structured permitted-characteristic entries for a profile.
116 Args:
117 profile: Profile key (e.g. ``"ess"``, ``"uds"``, ``"imds"``).
119 Returns:
120 List of :class:`PermittedCharacteristicEntry` or an empty list.
121 """
122 self._ensure_loaded()
123 with self._lock:
124 return list(self._entries.get(profile, []))
126 def get_all_profiles(self) -> list[str]:
127 """Return all loaded profile keys (sorted)."""
128 self._ensure_loaded()
129 with self._lock:
130 return sorted(self._entries)
133# Singleton instance for global use
134permitted_characteristics_registry = PermittedCharacteristicsRegistry()