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

1"""SDO UUIDs registry for Bluetooth SIG Special Development Organization UUIDs.""" 

2 

3from __future__ import annotations 

4 

5import re 

6 

7import msgspec 

8 

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 

12 

13 

14class SdoInfo(msgspec.Struct, frozen=True, kw_only=True): 

15 """Information about a Bluetooth SIG SDO UUID.""" 

16 

17 uuid: BluetoothUUID 

18 name: str 

19 id: str 

20 

21 

22class SdoUuidsRegistry(BaseRegistry[SdoInfo]): 

23 """Registry for Bluetooth SIG Special Development Organization UUIDs.""" 

24 

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() 

32 

33 def _normalize_name_for_id(self, name: str) -> str: 

34 """Normalize a name to create a valid ID string. 

35 

36 Args: 

37 name: The name to normalize 

38 

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 

49 

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 

55 

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"] 

63 

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}" 

67 

68 info = SdoInfo(uuid=uuid, name=name, id=sdo_id) 

69 

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 

74 

75 except (KeyError, ValueError): 

76 # Skip malformed entries 

77 continue 

78 

79 def get_sdo_info(self, uuid: str | int | BluetoothUUID) -> SdoInfo | None: 

80 """Get SDO information by UUID. 

81 

82 Args: 

83 uuid: The UUID to look up (string, int, or BluetoothUUID) 

84 

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 

93 

94 def get_sdo_info_by_name(self, name: str) -> SdoInfo | None: 

95 """Get SDO information by name (case insensitive). 

96 

97 Args: 

98 name: The SDO name to look up 

99 

100 Returns: 

101 SdoInfo if found, None otherwise 

102 """ 

103 return self._name_to_info.get(name.lower()) 

104 

105 def get_sdo_info_by_id(self, sdo_id: str) -> SdoInfo | None: 

106 """Get SDO information by SDO ID. 

107 

108 Args: 

109 sdo_id: The SDO ID to look up 

110 

111 Returns: 

112 SdoInfo if found, None otherwise 

113 """ 

114 return self._id_to_info.get(sdo_id) 

115 

116 def is_sdo_uuid(self, uuid: str | int | BluetoothUUID) -> bool: 

117 """Check if a UUID corresponds to a known SDO. 

118 

119 Args: 

120 uuid: The UUID to check 

121 

122 Returns: 

123 True if the UUID is a known SDO, False otherwise 

124 """ 

125 return self.get_sdo_info(uuid) is not None 

126 

127 def get_all_sdo_uuids(self) -> list[SdoInfo]: 

128 """Get all SDO UUIDs in the registry. 

129 

130 Returns: 

131 List of all SdoInfo objects 

132 """ 

133 return list(self._sdo_uuids.values()) 

134 

135 

136# Global instance for convenience 

137sdo_uuids_registry = SdoUuidsRegistry.get_instance()