Skip to content

Commit edffc54

Browse files
committed
Change to UV (and minor improvements)
1 parent 8632c04 commit edffc54

File tree

5 files changed

+85
-72
lines changed

5 files changed

+85
-72
lines changed

.github/workflows/merge.yml

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,15 @@ jobs:
2828
uses: actions/setup-python@v5
2929
with:
3030
python-version: ${{ env.DEFAULT_PYTHON }}
31-
- name: Prepare poetry
31+
- name: Prepare uv
3232
run: |
3333
pip install uv
3434
uv venv --seed venv
35-
. venv/bin/activate
36-
uv pip install poetry
37-
- name: Dependencies and build
35+
- name: Build
3836
run: |
3937
. venv/bin/activate
40-
poetry install --no-root
41-
poetry build
38+
uv build
4239
- name: Publish distribution 📦 to PyPI
43-
uses: pypa/gh-action-pypi-publish@release/v1
44-
with:
45-
skip-existing: true
40+
run: |
41+
. venv/bin/activate
42+
uv publish

.github/workflows/verify.yml

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,15 @@ jobs:
3333
steps:
3434
- name: Check out committed code
3535
uses: actions/checkout@v4
36-
- name: Prepare poetry
36+
- name: Prepare uv
3737
run: |
3838
pip install uv
3939
uv venv --seed venv
40+
- name: Build
41+
run: |
4042
. venv/bin/activate
41-
uv pip install poetry
42-
- name: Dependencies and build
43+
uv build
44+
- name: Publish distribution 📦 to TestPyPI
4345
run: |
4446
. venv/bin/activate
45-
poetry install --no-root
46-
poetry build
47-
- name: Publish distribution 📦 to Test PyPI
48-
uses: pypa/gh-action-pypi-publish@release/v1
49-
continue-on-error: true
50-
with:
51-
repository-url: https://test.pypi.org/legacy/
52-
skip-existing: true
47+
uv publish --publish-url https://test.pypi.org/legacy/

airos/airos8.py

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
from __future__ import annotations
44

5-
from typing import cast
6-
75
import asyncio
86
import json
97
import logging
10-
import ssl
8+
import ssl
119

1210
import aiohttp
1311

@@ -19,14 +17,15 @@
1917
class AirOS8:
2018
"""Set up connection to AirOS."""
2119

22-
def __init__(self, host: str, username: str, password: str): -> None
20+
def __init__(self, host: str, username: str, password: str):
2321
"""Initialize AirOS8 class."""
2422
self.username = username
2523
self.password = password
2624
self.base_url = f"https://{host}"
2725

2826
self._login_url = f"{self.base_url}/api/auth" # AirOS 8
2927
self._status_cgi_url = f"{self.base_url}/status.cgi" # AirOS 8
28+
self.current_csrf_token = None
3029

3130
self._use_json_for_login_post = False
3231

@@ -44,20 +43,15 @@ def __init__(self, host: str, username: str, password: str): -> None
4443
"X-Requested-With": "XMLHttpRequest",
4544
}
4645

47-
async def login(self): -> bool
46+
async def login(self) -> bool:
4847
"""Log in to the device assuring cookies and tokens set correctly."""
4948
loop = asyncio.get_running_loop()
50-
ssl_context = await loop.run_in_executor(
51-
None,
52-
ssl.create_default_context
53-
)
49+
ssl_context = await loop.run_in_executor(None, ssl.create_default_context)
5450
ssl_context.check_hostname = False
5551
ssl_context.verify_mode = ssl.CERT_NONE
5652
connector = aiohttp.TCPConnector(ssl=ssl_context)
5753

5854
async with aiohttp.ClientSession(connector=connector) as self.session:
59-
current_csrf_token = None
60-
6155
# --- Step 0: Pre-inject the 'ok=1' cookie before login POST (mimics curl) ---
6256
self.session.cookie_jar.update_cookies({"ok": "1"})
6357

@@ -74,26 +68,43 @@ async def login(self): -> bool
7468
login_request_headers["Content-Type"] = "application/json"
7569
post_data = json.dumps(login_payload)
7670
else:
77-
login_request_headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
71+
login_request_headers["Content-Type"] = (
72+
"application/x-www-form-urlencoded; charset=UTF-8"
73+
)
7874
post_data = login_payload
7975

8076
try:
81-
async with self.session.post(self._login_url, data=post_data, headers=login_request_headers) as response:
77+
async with self.session.post(
78+
self._login_url, data=post_data, headers=login_request_headers
79+
) as response:
8280
if not response.cookies:
8381
logger.exception("Empty cookies after login, bailing out.")
84-
raise DataMissingError
82+
raise DataMissingError from None
8583
else:
8684
for _, morsel in response.cookies.items():
87-
8885
# If the AIROS_ cookie was parsed but isn't automatically added to the jar, add it manually
89-
if morsel.key.startswith("AIROS_") and morsel.key not in self.session.cookie_jar:
86+
if (
87+
morsel.key.startswith("AIROS_")
88+
and morsel.key not in self.session.cookie_jar
89+
):
9090
# `SimpleCookie`'s Morsel objects are designed to be compatible with cookie jars.
9191
# We need to set the domain if it's missing, otherwise the cookie might not be sent.
9292
# For IP addresses, the domain is typically blank.
9393
# aiohttp's jar should handle it, but for explicit control:
9494
if not morsel.get("domain"):
95-
morsel["domain"] = response.url.host # Set to the host that issued it
96-
self.session.cookie_jar.update_cookies({morsel.key: morsel.output(header="")[len(morsel.key)+1:].split(";")[0].strip()}, response.url)
95+
morsel["domain"] = (
96+
response.url.host
97+
) # Set to the host that issued it
98+
self.session.cookie_jar.update_cookies(
99+
{
100+
morsel.key: morsel.output(header="")[
101+
len(morsel.key) + 1 :
102+
]
103+
.split(";")[0]
104+
.strip()
105+
},
106+
response.url,
107+
)
97108
# The update_cookies method can take a SimpleCookie morsel directly or a dict.
98109
# The morsel.output method gives 'NAME=VALUE; Path=...; HttpOnly'
99110
# We just need 'NAME=VALUE' or the morsel object itself.
@@ -104,68 +115,75 @@ async def login(self): -> bool
104115
# Simpler: just ensure the key-value pair is there for simple jar.
105116

106117
# Let's try the direct update of the key-value
107-
self.session.cookie_jar.update_cookies({morsel.key: morsel.value})
118+
self.session.cookie_jar.update_cookies(
119+
{morsel.key: morsel.value}
120+
)
108121

109122
new_csrf_token = response.headers.get("X-CSRF-ID")
110123
if new_csrf_token:
111-
current_csrf_token = new_csrf_token
124+
self.current_csrf_token = new_csrf_token
112125
else:
113126
return
114127

115128
# Re-check cookies in self.session.cookie_jar AFTER potential manual injection
116129
airos_cookie_found = False
117130
ok_cookie_found = False
118131
if not self.session.cookie_jar:
119-
logger.exception("COOKIE JAR IS EMPTY after login POST. This is a major issue.")
120-
raise DataMissingError
132+
logger.exception(
133+
"COOKIE JAR IS EMPTY after login POST. This is a major issue."
134+
)
135+
raise DataMissingError from None
121136
for cookie in self.session.cookie_jar:
122137
if cookie.key.startswith("AIROS_"):
123138
airos_cookie_found = True
124139
if cookie.key == "ok":
125140
ok_cookie_found = True
126141

127142
if not airos_cookie_found and not ok_cookie_found:
128-
raise DataMissingError
143+
raise DataMissingError from None
129144

130145
response_text = await response.text()
131146

132147
if response.status == 200:
133148
try:
134149
json.loads(response_text)
135150
return True
136-
except json.JSONDecodeError:
151+
except json.JSONDecodeError as err:
137152
logger.exception("JSON Decode Error")
138-
raise DataMissingError
153+
raise DataMissingError from err
139154

140155
else:
141156
log = f"Login failed with status {response.status}. Full Response: {response.text}"
142157
logger.error(log)
143-
raise ConnectionFailedError
144-
except aiohttp.ClientError:
158+
raise ConnectionFailedError from None
159+
except aiohttp.ClientError as err:
145160
logger.exception("Error during login")
146-
raise ConnectionFailedError
161+
raise ConnectionFailedError from err
147162

148-
async def status(self): -> dict
163+
async def status(self) -> dict:
149164
"""Retrieve status from the device."""
150-
# --- Step 2: Verify authenticated access by fetching status.cgi ---
151-
authenticated_get_headers = {**self._common_headers}
152-
if current_csrf_token:
153-
authenticated_get_headers["X-CSRF-ID"] = current_csrf_token
154-
155-
try:
156-
async with self.session.get(self._status_cgi_url, headers=authenticated_get_headers) as response:
157-
status_response_text = await response.text()
158-
159-
if response.status == 200:
160-
try:
161-
return json.loads(status_response_text)
162-
except json.JSONDecodeError:
163-
logger.exception("JSON Decode Error in authenticated status response")
164-
raise DataMissingError
165-
else:
166-
log = f"Authenticated status.cgi failed: {response.status}. Response: {status_response_text}"
167-
logger.error(log)
168-
except aiohttp.ClientError:
169-
logger.exception("Error during authenticated status.cgi call")
170-
raise ConnectionFailedError
171-
165+
# --- Step 2: Verify authenticated access by fetching status.cgi ---
166+
authenticated_get_headers = {**self._common_headers}
167+
if self.current_csrf_token:
168+
authenticated_get_headers["X-CSRF-ID"] = self.current_csrf_token
169+
170+
try:
171+
async with self.session.get(
172+
self._status_cgi_url, headers=authenticated_get_headers
173+
) as response:
174+
status_response_text = await response.text()
175+
176+
if response.status == 200:
177+
try:
178+
return json.loads(status_response_text)
179+
except json.JSONDecodeError:
180+
logger.exception(
181+
"JSON Decode Error in authenticated status response"
182+
)
183+
raise DataMissingError from None
184+
else:
185+
log = f"Authenticated status.cgi failed: {response.status}. Response: {status_response_text}"
186+
logger.error(log)
187+
except aiohttp.ClientError as err:
188+
logger.exception("Error during authenticated status.cgi call")
189+
raise ConnectionFailedError from err

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "airos"
7-
version = "0.0.3a0"
7+
version = "0.0.3a1"
88
license = "MIT"
99
description = "Ubiquity airOS module(s) for Python 3."
1010
readme = "README.md"

requirements-test.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pytest
2+
pytest-asyncio
3+
aiohttp

0 commit comments

Comments
 (0)