Skip to content

Commit 22f1fb1

Browse files
committed
CRAI/Gem improvements
1 parent 264430e commit 22f1fb1

File tree

7 files changed

+56
-40
lines changed

7 files changed

+56
-40
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,19 @@ if __name__ == "__main__":
142142

143143
##### Update
144144

145-
Will return either ```{'update': False}``` or the full information regarding the available update:
145+
Will return either ```{"update": False}``` or the full information regarding the available update:
146146

147147
```json
148-
{'checksum': 'b1bea879a9f518f714ce638172e3a860', 'version': 'v8.7.19', 'security': '', 'date': '250811', 'url': 'https://dl.ubnt.com/firmwares/XC-fw/v8.7.19/WA.v8.7.19.48279.250811.0636.bin', 'update': True, 'changelog': 'https://dl.ubnt.com/firmwares/XC-fw/v8.7.19/changelog.txt'}
148+
{"checksum": "b1bea879a9f518f714ce638172e3a860", "version": "v8.7.19", "security": "", "date": "250811", "url": "https://dl.ubnt.com/firmwares/XC-fw/v8.7.19/WA.v8.7.19.48279.250811.0636.bin", "update": True, "changelog": "https://dl.ubnt.com/firmwares/XC-fw/v8.7.19/changelog.txt"}
149149
```
150150

151151
##### Progress
152152

153-
If no progress to report ```{'progress': -1}``` otherwise a positive value between 0 and 100.
153+
If no progress to report ```{"progress": -1}``` otherwise a positive value between 0 and 100.
154154

155155
##### Install
156156

157-
Only positives outcome from user-experience, which should return:
157+
Only a positive outcome is expected from the user experience; the call should return:
158158

159159
```json
160160
{

airos/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""Ubiquity airOS."""
1+
"""Ubiquiti airOS."""

airos/airos8.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
from __future__ import annotations
44

55
import asyncio
6+
from http.cookies import SimpleCookie
67
import json
78
import logging
8-
from typing import Any
9+
from typing import Any, NamedTuple
910
from urllib.parse import urlparse
1011

1112
import aiohttp
1213
from mashumaro.exceptions import InvalidFieldValue, MissingField
14+
from yarl import URL
1315

1416
from .data import (
1517
AirOS8Data as AirOSData,
@@ -28,6 +30,16 @@
2830
_LOGGER = logging.getLogger(__name__)
2931

3032

33+
class ApiResponse(NamedTuple):
34+
"""Define API call structure."""
35+
36+
status: int
37+
headers: dict[str, Any]
38+
cookies: SimpleCookie
39+
url: URL
40+
text: str
41+
42+
3143
class AirOS:
3244
"""AirOS 8 connection class."""
3345

@@ -159,7 +171,7 @@ def _get_authenticated_headers(
159171

160172
async def _api_call(
161173
self, method: str, url: str, headers: dict[str, Any], **kwargs: Any
162-
) -> dict[str, Any]:
174+
) -> ApiResponse:
163175
"""Make API call."""
164176
if url != self._login_url and not self.connected:
165177
_LOGGER.error("Not connected, login first")
@@ -170,8 +182,13 @@ async def _api_call(
170182
method, url, headers=headers, **kwargs
171183
) as response:
172184
response_text = await response.text()
173-
result = {"response": response, "response_text": response_text}
174-
return result
185+
return ApiResponse(
186+
status=response.status,
187+
headers=dict(response.headers),
188+
cookies=response.cookies,
189+
url=response.url,
190+
text=response_text,
191+
)
175192
except (TimeoutError, aiohttp.ClientError) as err:
176193
_LOGGER.exception("Error during API call to %s: %s", url, err)
177194
raise AirOSDeviceConnectionError from err
@@ -183,9 +200,7 @@ async def _request_json(
183200
self, method: str, url: str, headers: dict[str, Any], **kwargs: Any
184201
) -> dict[str, Any] | Any:
185202
"""Return JSON from API call."""
186-
result = await self._api_call(method, url, headers=headers, **kwargs)
187-
response = result.get("response", {})
188-
response_text = result.get("response_text", "")
203+
response = await self._api_call(method, url, headers=headers, **kwargs)
189204

190205
match response.status:
191206
case 200:
@@ -198,12 +213,12 @@ async def _request_json(
198213
"API call to %s failed with status %d: %s",
199214
url,
200215
response.status,
201-
response_text,
216+
response.text,
202217
)
203218
raise AirOSDeviceConnectionError from None
204219

205220
try:
206-
return json.loads(await response.text())
221+
return json.loads(response.text)
207222
except json.JSONDecodeError as err:
208223
_LOGGER.exception("JSON Decode Error in API response from %s", url)
209224
raise AirOSDataMissingError from err
@@ -222,26 +237,23 @@ async def login(self) -> bool:
222237
request_headers = self._get_authenticated_headers(ct_form=True)
223238
if self._use_json_for_login_post:
224239
request_headers = self._get_authenticated_headers(ct_json=True)
225-
result = await self._api_call(
240+
response = await self._api_call(
226241
"POST", self._login_url, headers=request_headers, json=payload
227242
)
228243
else:
229-
result = await self._api_call(
244+
response = await self._api_call(
230245
"POST", self._login_url, headers=request_headers, data=payload
231246
)
232-
response = result.get("response", {})
233-
response_text = result.get("response_text", "")
234247

235248
if response.status == 403:
236249
_LOGGER.error("Authentication denied.")
237250
raise AirOSConnectionAuthenticationError from None
238251

239252
for _, morsel in response.cookies.items():
240253
# If the AIROS_ cookie was parsed but isn't automatically added to the jar, add it manually
241-
if (
242-
morsel.key.startswith("AIROS_")
243-
and morsel.key not in self.session.cookie_jar
244-
):
254+
if morsel.key.startswith("AIROS_") and morsel.key not in [
255+
cookie.key for cookie in self.session.cookie_jar
256+
]:
245257
# `SimpleCookie`'s Morsel objects are designed to be compatible with cookie jars.
246258
# We need to set the domain if it's missing, otherwise the cookie might not be sent.
247259
# For IP addresses, the domain is typically blank.
@@ -299,7 +311,7 @@ async def login(self) -> bool:
299311
raise AirOSConnectionAuthenticationError from None
300312

301313
try:
302-
json.loads(response_text)
314+
json.loads(response.text)
303315
self.connected = True
304316
return True
305317
except json.JSONDecodeError as err:
@@ -344,15 +356,13 @@ async def stakick(self, mac_address: str | None = None) -> bool:
344356
request_headers = self._get_authenticated_headers(ct_form=True)
345357
payload = {"staif": "ath0", "staid": mac_address.upper()}
346358

347-
result = await self._api_call(
359+
response = await self._api_call(
348360
"POST", self._stakick_cgi_url, headers=request_headers, data=payload
349361
)
350-
response = result.get("response", {})
351362
if response.status == 200:
352363
return True
353364

354-
response_text = result.get("response_text", "")
355-
log = f"Unable to restart connection response status {response.status} with {response_text}"
365+
log = f"Unable to restart connection response status {response.status} with {response.text}"
356366
_LOGGER.error(log)
357367
return False
358368

@@ -365,15 +375,13 @@ async def provmode(self, active: bool = False) -> bool:
365375
action = "start"
366376

367377
payload = {"action": action}
368-
result = await self._api_call(
378+
response = await self._api_call(
369379
"POST", self._provmode_url, headers=request_headers, data=payload
370380
)
371-
response = result.get("response", {})
372381
if response.status == 200:
373382
return True
374383

375-
response_text = result.get("response_text", "")
376-
log = f"Unable to change provisioning mode response status {response.status} with {response_text}"
384+
log = f"Unable to change provisioning mode response status {response.status} with {response.text}"
377385
_LOGGER.error(log)
378386
return False
379387

@@ -392,6 +400,10 @@ async def update_check(self, force: bool = False) -> dict[str, Any]:
392400
if force:
393401
payload = {"force": "yes"}
394402
request_headers = self._get_authenticated_headers(ct_form=True)
403+
return await self._request_json(
404+
"POST", self._update_check_url, headers=request_headers, data=payload
405+
)
406+
395407
return await self._request_json(
396408
"POST", self._update_check_url, headers=request_headers, json=payload
397409
)
@@ -402,7 +414,7 @@ async def progress(self) -> dict[str, Any]:
402414
payload: dict[str, Any] = {}
403415

404416
return await self._request_json(
405-
"POST", self._download_progress_url, headers=request_headers, json=payload
417+
"POST", self._download_progress_url, headers=request_headers, data=payload
406418
)
407419

408420
async def download(self) -> dict[str, Any]:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta"
66
name = "airos"
77
version = "0.4.0a0"
88
license = "MIT"
9-
description = "Ubiquity airOS module(s) for Python 3."
9+
description = "Ubiquiti airOS module(s) for Python 3."
1010
readme = "README.md"
11-
keywords = ["home", "automation", "ubiquity", "uisp", "airos", "module"]
11+
keywords = ["home", "automation", "ubiquiti", "uisp", "airos", "module"]
1212
classifiers = [
1313
"Development Status :: 5 - Production/Stable",
1414
"Intended Audience :: Developers",

tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""Tests for the Ubiquity AirOS python module."""
1+
"""Tests for the Ubiquiti AirOS python module."""

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Ubiquity AirOS test fixtures."""
1+
"""Ubiquiti AirOS test fixtures."""
22

33
from _collections_abc import AsyncGenerator, Generator
44
import asyncio

tests/test_stations.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Ubiquity AirOS tests."""
1+
"""Ubiquiti AirOS tests."""
22

33
from http.cookies import SimpleCookie
44
import json
@@ -12,6 +12,7 @@
1212
import pytest
1313

1414
import aiofiles
15+
from yarl import URL
1516

1617

1718
async def _read_fixture(fixture: str = "loco5ac_ap-ptp") -> Any:
@@ -169,16 +170,19 @@ async def test_ap_object(
169170
mock_login_response.headers = {"X-CSRF-ID": "test-csrf-token"}
170171
# --- Prepare fake GET /api/status response ---
171172
fixture_data = await _read_fixture(fixture)
172-
mock_status_payload = fixture_data
173173
mock_status_response = MagicMock()
174174
mock_status_response.__aenter__.return_value = mock_status_response
175175
mock_status_response.text = AsyncMock(return_value=json.dumps(fixture_data))
176176
mock_status_response.status = 200
177-
mock_status_response.json = AsyncMock(return_value=mock_status_payload)
177+
mock_status_response.cookies = SimpleCookie()
178+
mock_status_response.headers = {}
179+
mock_status_response.url = URL(base_url)
178180

179181
with (
180182
patch.object(
181-
airos_device.session, "request", return_value=mock_status_response
183+
airos_device.session,
184+
"request",
185+
side_effect=[mock_login_response, mock_status_response],
182186
),
183187
):
184188
assert await airos_device.login()

0 commit comments

Comments
 (0)