Skip to content

Commit 25568bf

Browse files
committed
Improve typing and ship py.typed
1 parent dc0bf52 commit 25568bf

8 files changed

Lines changed: 69 additions & 50 deletions

File tree

pyControl4/account.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
from contextlib import asynccontextmanager
8-
from typing import AsyncGenerator
8+
from typing import Any, AsyncGenerator
99

1010
import aiohttp
1111
import asyncio
@@ -153,8 +153,9 @@ async def get_account_bearer_token(self) -> str:
153153
data = await self._send_account_auth_request()
154154
json_dict = json.loads(data)
155155
try:
156-
self.account_bearer_token = json_dict["authToken"]["token"]
157-
return self.account_bearer_token
156+
token: str = json_dict["authToken"]["token"]
157+
self.account_bearer_token = token
158+
return token
158159
except KeyError:
159160
msg = (
160161
"Did not receive an account bearer token. "
@@ -163,7 +164,7 @@ async def get_account_bearer_token(self) -> str:
163164
_LOGGER.error(msg + data)
164165
raise
165166

166-
async def get_account_controllers(self) -> dict:
167+
async def get_account_controllers(self) -> dict[str, Any]:
167168
"""Returns a dictionary of the information for all controllers registered
168169
to an account.
169170
@@ -179,13 +180,14 @@ async def get_account_controllers(self) -> dict:
179180
data = await self._send_account_get_request(GET_CONTROLLERS_ENDPOINT)
180181
json_dict = json.loads(data)
181182
try:
182-
return json_dict["account"]
183+
result: dict[str, Any] = json_dict["account"]
184+
return result
183185
except KeyError:
184186
msg = "Did not receive account information from the Control4 API."
185187
_LOGGER.error(msg + " Response: " + data)
186188
raise
187189

188-
async def get_controller_info(self, controller_href: str) -> dict:
190+
async def get_controller_info(self, controller_href: str) -> dict[str, Any]:
189191
"""Returns a dictionary of the information of a specific controller.
190192
191193
Parameters:
@@ -228,7 +230,7 @@ async def get_controller_info(self, controller_href: str) -> dict:
228230
```
229231
"""
230232
data = await self._send_account_get_request(controller_href)
231-
json_dict = json.loads(data)
233+
json_dict: dict[str, Any] = json.loads(data)
232234
return json_dict
233235

234236
async def get_controller_os_version(self, controller_href: str) -> str:
@@ -241,13 +243,14 @@ async def get_controller_os_version(self, controller_href: str) -> str:
241243
data = await self._send_account_get_request(controller_href + "/controller")
242244
json_dict = json.loads(data)
243245
try:
244-
return json_dict["osVersion"]
246+
version: str = json_dict["osVersion"]
247+
return version
245248
except KeyError:
246249
msg = "Did not receive OS version from the Control4 API."
247250
_LOGGER.error(msg + " Response: " + data)
248251
raise
249252

250-
async def get_director_bearer_token(self, controller_common_name: str) -> dict:
253+
async def get_director_bearer_token(self, controller_common_name: str) -> dict[str, Any]:
251254
"""Returns a dictionary with a director bearer token for making Control4
252255
Director API requests, and its time valid in seconds (usually 86400 seconds)
253256

pyControl4/director.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async def send_get_request(self, uri: str) -> str:
7676
return text
7777

7878
async def send_post_request(
79-
self, uri: str, command: str, params: dict, is_async: bool = True
79+
self, uri: str, command: str, params: dict[str, Any], is_async: bool = True
8080
) -> str:
8181
"""Sends a POST request to the specified API URI. Used to send commands
8282
to the Director.
@@ -104,7 +104,7 @@ async def send_post_request(
104104
check_response_for_error(text)
105105
return text
106106

107-
async def get_all_items_by_category(self, category: str) -> list[dict]:
107+
async def get_all_items_by_category(self, category: str) -> list[dict[str, Any]]:
108108
"""Returns a list of items related to a particular category.
109109
110110
Parameters:
@@ -115,23 +115,26 @@ async def get_all_items_by_category(self, category: str) -> list[dict]:
115115
outlet_wireless_dimmer, voice-scene
116116
"""
117117
data = await self.send_get_request(f"/api/v1/categories/{category}")
118-
return json.loads(data)
118+
result: list[dict[str, Any]] = json.loads(data)
119+
return result
119120

120-
async def get_all_item_info(self) -> list[dict]:
121+
async def get_all_item_info(self) -> list[dict[str, Any]]:
121122
"""Returns a list of all the items on the Director."""
122123
data = await self.send_get_request("/api/v1/items")
123-
return json.loads(data)
124+
result: list[dict[str, Any]] = json.loads(data)
125+
return result
124126

125-
async def get_item_info(self, item_id: int) -> list[dict]:
127+
async def get_item_info(self, item_id: int) -> list[dict[str, Any]]:
126128
"""Returns a list of the details of the specified item.
127129
128130
Parameters:
129131
`item_id` - The Control4 item ID.
130132
"""
131133
data = await self.send_get_request(f"/api/v1/items/{item_id}")
132-
return json.loads(data)
134+
result: list[dict[str, Any]] = json.loads(data)
135+
return result
133136

134-
async def get_item_setup(self, item_id: int) -> dict:
137+
async def get_item_setup(self, item_id: int) -> dict[str, Any]:
135138
"""Returns the setup info of the specified item.
136139
137140
Parameters:
@@ -140,16 +143,18 @@ async def get_item_setup(self, item_id: int) -> dict:
140143
data = await self.send_post_request(
141144
f"/api/v1/items/{item_id}/commands", "GET_SETUP", {}, False
142145
)
143-
return json.loads(data)
146+
result: dict[str, Any] = json.loads(data)
147+
return result
144148

145-
async def get_item_variables(self, item_id: int) -> list[dict]:
149+
async def get_item_variables(self, item_id: int) -> list[dict[str, Any]]:
146150
"""Returns a list of the variables available for the specified item.
147151
148152
Parameters:
149153
`item_id` - The Control4 item ID.
150154
"""
151155
data = await self.send_get_request(f"/api/v1/items/{item_id}/variables")
152-
return json.loads(data)
156+
result: list[dict[str, Any]] = json.loads(data)
157+
return result
153158

154159
async def get_item_variable_value(
155160
self, item_id: int, var_name: str | list[str] | tuple[str, ...] | set[str]
@@ -207,40 +212,43 @@ async def get_all_item_variable_value(
207212
f"Empty response received from Director! The variable {var_name} "
208213
f"doesn't seem to exist for any items."
209214
)
210-
json_dict = json.loads(data)
215+
json_dict: list[dict[str, Any]] = json.loads(data)
211216
for item in json_dict:
212217
if item.get("value") == "Undefined":
213218
item["value"] = None
214219
return json_dict
215220

216-
async def get_item_commands(self, item_id: int) -> list[dict]:
221+
async def get_item_commands(self, item_id: int) -> list[dict[str, Any]]:
217222
"""Returns the commands available for the specified item.
218223
219224
Parameters:
220225
`item_id` - The Control4 item ID.
221226
"""
222227
data = await self.send_get_request(f"/api/v1/items/{item_id}/commands")
223-
return json.loads(data)
228+
result: list[dict[str, Any]] = json.loads(data)
229+
return result
224230

225-
async def get_item_network(self, item_id: int) -> list[dict]:
231+
async def get_item_network(self, item_id: int) -> list[dict[str, Any]]:
226232
"""Returns the network information for the specified item.
227233
228234
Parameters:
229235
`item_id` - The Control4 item ID.
230236
"""
231237
data = await self.send_get_request(f"/api/v1/items/{item_id}/network")
232-
return json.loads(data)
238+
result: list[dict[str, Any]] = json.loads(data)
239+
return result
233240

234-
async def get_item_bindings(self, item_id: int) -> list[dict]:
241+
async def get_item_bindings(self, item_id: int) -> list[dict[str, Any]]:
235242
"""Returns the bindings information for the specified item.
236243
237244
Parameters:
238245
`item_id` - The Control4 item ID.
239246
"""
240247
data = await self.send_get_request(f"/api/v1/items/{item_id}/bindings")
241-
return json.loads(data)
248+
result: list[dict[str, Any]] = json.loads(data)
249+
return result
242250

243-
async def get_ui_configuration(self) -> dict:
251+
async def get_ui_configuration(self) -> dict[str, Any]:
244252
"""Returns a dictionary of the Control4 App UI Configuration enumerating
245253
rooms and capabilities
246254
@@ -323,4 +331,5 @@ async def get_ui_configuration(self) -> dict:
323331
}
324332
"""
325333
data = await self.send_get_request("/api/v1/agents/ui_configuration")
326-
return json.loads(data)
334+
result: dict[str, Any] = json.loads(data)
335+
return result

pyControl4/error_handling.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
from __future__ import annotations
44

5+
from typing import Any
6+
57
import json
68
import xmltodict
79

810

911
class C4Exception(Exception):
1012
"""Base error for pyControl4."""
1113

12-
def __init__(self, message):
14+
def __init__(self, message: str) -> None:
1315
self.message = message
1416

1517

@@ -88,7 +90,7 @@ def _check_response_format(response_text: str) -> str:
8890
return "JSON"
8991

9092

91-
def _extract_error_info(dictionary: dict) -> dict | None:
93+
def _extract_error_info(dictionary: dict[str, Any]) -> dict[str, Any] | None:
9294
"""Extract error information from a parsed Control4 response.
9395
9496
Returns a dict with 'details', 'code', or 'error' key, or None if no error found.
@@ -121,7 +123,7 @@ def _extract_error_info(dictionary: dict) -> dict | None:
121123
return None
122124

123125

124-
def _raise_error(error_info: dict, response_text: str) -> None:
126+
def _raise_error(error_info: dict[str, Any], response_text: str) -> None:
125127
"""Raise appropriate exception based on error info."""
126128
details = error_info.get("details")
127129
code = error_info.get("code")

pyControl4/py.typed

Whitespace-only changes.

pyControl4/room.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json
6+
from typing import Any
67

78
from pyControl4 import C4Entity
89

@@ -139,7 +140,7 @@ async def set_stop(self) -> None:
139140
{},
140141
)
141142

142-
async def get_audio_devices(self) -> dict:
143+
async def get_audio_devices(self) -> dict[str, Any]:
143144
"""
144145
Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions
145146
@@ -152,9 +153,10 @@ async def get_audio_devices(self) -> dict:
152153
data = await self.director.send_get_request(
153154
f"/api/v1/locations/rooms/{self.item_id}/audio_devices"
154155
)
155-
return json.loads(data)
156+
result: dict[str, Any] = json.loads(data)
157+
return result
156158

157-
async def get_video_devices(self) -> dict:
159+
async def get_video_devices(self) -> dict[str, Any]:
158160
"""
159161
Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions
160162
@@ -167,4 +169,5 @@ async def get_video_devices(self) -> dict:
167169
data = await self.director.send_get_request(
168170
f"/api/v1/locations/rooms/{self.item_id}/video_devices"
169171
)
170-
return json.loads(data)
172+
result: dict[str, Any] = json.loads(data)
173+
return result

pyControl4/websocket.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import aiohttp
1111
import asyncio
12-
import socketio_v4 as socketio
12+
import socketio_v4 as socketio # type: ignore[import-untyped]
1313
import logging
1414

1515
from .error_handling import check_response_for_error
@@ -21,14 +21,14 @@
2121
_STATUS_ACK_MESSAGE = "2"
2222

2323

24-
class _C4DirectorNamespace(socketio.AsyncClientNamespace):
24+
class _C4DirectorNamespace(socketio.AsyncClientNamespace): # type: ignore[misc]
2525
def __init__(self, *args: Any, **kwargs: Any) -> None:
2626
self.url: str = kwargs.pop("url")
2727
self.token: str = kwargs.pop("token")
28-
self.callback: Callable = kwargs.pop("callback")
28+
self.callback: Callable[..., Any] = kwargs.pop("callback")
2929
self.session: aiohttp.ClientSession | None = kwargs.pop("session")
30-
self.connect_callback: Callable | None = kwargs.pop("connect_callback")
31-
self.disconnect_callback: Callable | None = kwargs.pop("disconnect_callback")
30+
self.connect_callback: Callable[..., Any] | None = kwargs.pop("connect_callback")
31+
self.disconnect_callback: Callable[..., Any] | None = kwargs.pop("disconnect_callback")
3232
super().__init__(*args, **kwargs)
3333
self.uri = _NAMESPACE_URI
3434
self.subscription_id: str | None = None
@@ -99,8 +99,8 @@ def __init__(
9999
self,
100100
ip: str,
101101
session_no_verify_ssl: aiohttp.ClientSession | None = None,
102-
connect_callback: Callable | None = None,
103-
disconnect_callback: Callable | None = None,
102+
connect_callback: Callable[..., Any] | None = None,
103+
disconnect_callback: Callable[..., Any] | None = None,
104104
):
105105
"""Creates a Control4 Websocket object.
106106
@@ -128,21 +128,21 @@ def __init__(
128128
self.base_url: str = f"https://{ip}"
129129
self.wss_url: str = f"wss://{ip}"
130130
self.session: aiohttp.ClientSession | None = session_no_verify_ssl
131-
self.connect_callback: Callable | None = connect_callback
132-
self.disconnect_callback: Callable | None = disconnect_callback
131+
self.connect_callback: Callable[..., Any] | None = connect_callback
132+
self.disconnect_callback: Callable[..., Any] | None = disconnect_callback
133133

134-
self._item_callbacks: dict[int, list[Callable]] = dict()
134+
self._item_callbacks: dict[int, list[Callable[..., Any]]] = dict()
135135
self._sio: socketio.AsyncClient | None = None
136136

137137
@property
138-
def item_callbacks(self) -> MappingProxyType[int, list[Callable]]:
138+
def item_callbacks(self) -> MappingProxyType[int, list[Callable[..., Any]]]:
139139
"""Returns a read-only view of registered item ids (key) and their
140140
callbacks (value). Use add_item_callback() or remove_item_callback()
141141
to modify callbacks.
142142
"""
143143
return MappingProxyType(self._item_callbacks)
144144

145-
def add_item_callback(self, item_id: int, callback: Callable) -> None:
145+
def add_item_callback(self, item_id: int, callback: Callable[..., Any]) -> None:
146146
"""Register a callback to receive updates about an item.
147147
Parameters:
148148
`item_id` - The Control4 item ID.
@@ -159,7 +159,7 @@ def add_item_callback(self, item_id: int, callback: Callable) -> None:
159159
self._item_callbacks[item_id].append(callback)
160160

161161
def remove_item_callback(
162-
self, item_id: int, callback: Callable | None = None
162+
self, item_id: int, callback: Callable[..., Any] | None = None
163163
) -> None:
164164
"""Unregister callback(s) for an item.
165165
Parameters:

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ python-socketio-v4
44
websocket-client
55
pdoc3
66
pytest-asyncio
7+
types-xmltodict

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55

66
setuptools.setup(
77
name="pyControl4", # Replace with your own username
8-
version="2.0.0",
8+
version="2.0.1",
99
author="lawtancool",
1010
author_email="contact@lawrencetan.ca",
1111
description="Python 3 asyncio package for interacting with Control4 systems",
1212
long_description=long_description,
1313
long_description_content_type="text/markdown",
1414
url="https://github.com/lawtancool/pyControl4",
1515
packages=setuptools.find_packages(),
16+
package_data={"pyControl4": ["py.typed"]},
1617
classifiers=[
1718
"Programming Language :: Python :: 3",
1819
"License :: OSI Approved :: Apache Software License",

0 commit comments

Comments
 (0)