Coverage for src/bluetooth_sig/registry/sdo_uuids.py: 76%
55 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
1"""SDO UUIDs registry for Bluetooth SIG Special Development Organization UUIDs."""
3from __future__ import annotations
5import re
7import msgspec
9from bluetooth_sig.registry.base import BaseRegistry
10from bluetooth_sig.registry.utils import find_bluetooth_sig_path, load_yaml_uuids, parse_bluetooth_uuid
11from bluetooth_sig.types.uuid import BluetoothUUID
14class SdoInfo(msgspec.Struct, frozen=True, kw_only=True):
15 """Information about a Bluetooth SIG SDO UUID."""
17 uuid: BluetoothUUID
18 name: str
19 id: str
22class SdoUuidsRegistry(BaseRegistry[SdoInfo]):
23 """Registry for Bluetooth SIG Special Development Organization UUIDs."""
25 def __init__(self) -> None:
26 """Initialize the SDO UUIDs registry."""
27 super().__init__()
28 self._sdo_uuids: dict[str, SdoInfo] = {}
29 self._name_to_info: dict[str, SdoInfo] = {}
30 self._id_to_info: dict[str, SdoInfo] = {}
31 self._load_sdo_uuids()
33 def _normalize_name_for_id(self, name: str) -> str:
34 """Normalize a name to create a valid ID string.
36 Args:
37 name: The name to normalize
39 Returns:
40 Normalized ID string
41 """
42 # Convert to lowercase, replace spaces and special chars with underscores
43 normalized = re.sub(r"[^a-zA-Z0-9]", "_", name.lower())
44 # Remove multiple consecutive underscores
45 normalized = re.sub(r"_+", "_", normalized)
46 # Remove leading/trailing underscores
47 normalized = normalized.strip("_")
48 return normalized
50 def _load_sdo_uuids(self) -> None:
51 """Load SDO UUIDs from the Bluetooth SIG YAML file."""
52 base_path = find_bluetooth_sig_path()
53 if not base_path:
54 return
56 # Load SDO UUIDs
57 sdo_uuids_yaml = base_path / "uuids" / "sdo_uuids.yaml"
58 if sdo_uuids_yaml.exists():
59 for item in load_yaml_uuids(sdo_uuids_yaml):
60 try:
61 uuid = parse_bluetooth_uuid(item["uuid"])
62 name = item["name"]
64 # Generate synthetic ID since SDO YAML doesn't have 'id' field
65 normalized_name = self._normalize_name_for_id(name)
66 sdo_id = f"org.bluetooth.sdo.{normalized_name}"
68 info = SdoInfo(uuid=uuid, name=name, id=sdo_id)
70 # Store by UUID string for fast lookup
71 self._sdo_uuids[uuid.short_form.upper()] = info
72 self._name_to_info[name.lower()] = info
73 self._id_to_info[sdo_id] = info
75 except (KeyError, ValueError):
76 # Skip malformed entries
77 continue
79 def get_sdo_info(self, uuid: str | int | BluetoothUUID) -> SdoInfo | None:
80 """Get SDO information by UUID.
82 Args:
83 uuid: The UUID to look up (string, int, or BluetoothUUID)
85 Returns:
86 SdoInfo if found, None otherwise
87 """
88 try:
89 bt_uuid = parse_bluetooth_uuid(uuid)
90 return self._sdo_uuids.get(bt_uuid.short_form.upper())
91 except ValueError:
92 return None
94 def get_sdo_info_by_name(self, name: str) -> SdoInfo | None:
95 """Get SDO information by name (case insensitive).
97 Args:
98 name: The SDO name to look up
100 Returns:
101 SdoInfo if found, None otherwise
102 """
103 return self._name_to_info.get(name.lower())
105 def get_sdo_info_by_id(self, sdo_id: str) -> SdoInfo | None:
106 """Get SDO information by SDO ID.
108 Args:
109 sdo_id: The SDO ID to look up
111 Returns:
112 SdoInfo if found, None otherwise
113 """
114 return self._id_to_info.get(sdo_id)
116 def is_sdo_uuid(self, uuid: str | int | BluetoothUUID) -> bool:
117 """Check if a UUID corresponds to a known SDO.
119 Args:
120 uuid: The UUID to check
122 Returns:
123 True if the UUID is a known SDO, False otherwise
124 """
125 return self.get_sdo_info(uuid) is not None
127 def get_all_sdo_uuids(self) -> list[SdoInfo]:
128 """Get all SDO UUIDs in the registry.
130 Returns:
131 List of all SdoInfo objects
132 """
133 return list(self._sdo_uuids.values())
136# Global instance for convenience
137sdo_uuids_registry = SdoUuidsRegistry.get_instance()