From f5966d47935e25301d6cddaa4df4e615a85c52d7 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 13 Aug 2025 19:32:51 +0200 Subject: [PATCH 1/4] Improve ip6 redaction --- airos/data.py | 17 ++++++++++++++++- script/mashumaro-step-debug.py | 11 ++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/airos/data.py b/airos/data.py index 0cbb4d1..1e18da7 100644 --- a/airos/data.py +++ b/airos/data.py @@ -61,7 +61,7 @@ def _redact(d: dict[str, Any]) -> dict[str, Any]: for k, v in d.items(): if k in sensitive_keys: if isinstance(v, str) and (is_mac_address(v) or is_mac_address_mask(v)): - # Redact only the first 6 hex characters of a MAC address + # Redact only the last part of a MAC address to a dummy value redacted_d[k] = "00:11:22:33:" + v.replace("-", ":").upper()[-5:] elif isinstance(v, str) and is_ip_address(v): # Redact to a dummy local IP address @@ -71,6 +71,21 @@ def _redact(d: dict[str, Any]) -> dict[str, Any]: ): # Redact list of IPs to a dummy list redacted_d[k] = ["127.0.0.3"] # type: ignore[assignment] + elif isinstance(v, list) and all( + isinstance(i, dict) and "addr" in i and is_ip_address(i["addr"]) + for i in v + ): + # Redact list of dictionaries with IP addresses to a dummy list + redacted_list = [] + for item in v: + redacted_item = item.copy() + redacted_item["addr"] = ( + "127.0.0.3" + if ipaddress.ip_address(redacted_item["addr"]).version == 4 + else "::1" + ) + redacted_list.append(redacted_item) + redacted_d[k] = redacted_list # type: ignore[assignment] else: redacted_d[k] = "REDACTED" elif isinstance(v, dict): diff --git a/script/mashumaro-step-debug.py b/script/mashumaro-step-debug.py index 628981e..300c864 100644 --- a/script/mashumaro-step-debug.py +++ b/script/mashumaro-step-debug.py @@ -12,7 +12,7 @@ if project_root_dir not in sys.path: sys.path.append(project_root_dir) -from airos.data import AirOS8Data, Remote, Station, Wireless # noqa: E402 +from airos.data import AirOS8Data, Interface, Remote, Station, Wireless # noqa: E402 logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) _LOGGER = logging.getLogger(__name__) @@ -63,6 +63,15 @@ def main() -> None: wireless_obj = Wireless.from_dict(wireless_data) # noqa: F841 _LOGGER.info(" -> Success! The Wireless object is valid.") + _LOGGER.info(" -> Checking list of Interface objects...") + interfaces = data["interfaces"] + interface_obj_list = [] # noqa: F841 + for i, interface_data in enumerate(interfaces): + _LOGGER.info(" -> Checking Interface object at index %s...", i) + _LOGGER.info(" Interface should be %s.", interface_data["ifname"]) + interface_obj = Interface.from_dict(interface_data) # noqa: F841 + _LOGGER.info(" Success! Interface is valid.") + _LOGGER.info("Attempting to deserialize full AirOS8Data object...") airos_data_obj = AirOS8Data.from_dict(data) # noqa: F841 _LOGGER.info("Success! Full AirOS8Data object is valid.") From b92fc6ff5b866a2ed8031d763507813ed04e1ffa Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 13 Aug 2025 19:59:42 +0200 Subject: [PATCH 2/4] Fix artifact upgrade --- .github/workflows/verify.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ee4ba89..59983bc 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -145,11 +145,15 @@ jobs: . venv/bin/activate uv pip install -r requirements.txt -r requirements-test.txt - name: Download all coverage artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 + with: + pattern: coverage-* + merge-multiple: true + path: ${{ github.workspace }}/artifacts - name: Combine coverage results run: | . venv/bin/activate - coverage combine coverage*/.coverage* + coverage combine artifacts/.coverage* coverage report --fail-under=85 coverage xml - name: Upload coverage to Codecov From 06f255c3fb14d920b30204efbe0be7d45fa43d7a Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 13 Aug 2025 20:01:28 +0200 Subject: [PATCH 3/4] Add pylint (generically) and force pytest --- .pre-commit-config.yaml | 19 ++++++++++++++++--- CHANGELOG.md | 7 +++++++ airos/airos8.py | 2 +- pyproject.toml | 2 +- script/generate_ha_fixture.py | 4 ++-- script/mashumaro-step-debug.py | 11 +++++------ tests/conftest.py | 2 ++ tests/test_discovery.py | 2 ++ 8 files changed, 36 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 619f856..095343f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: - --quiet - --format=custom - --configfile=tests/bandit.yaml - files: ^(airos|tests)/.+\.py$ + files: ^(airos|tests|script)/.+\.py$ - repo: https://github.com/adrienverge/yamllint.git rev: v1.37.1 hooks: @@ -75,17 +75,30 @@ repos: - --py39-plus - --force - --keep-updates - files: ^(airos|tests)/.+\.py$ + files: ^(airos|tests|script)/.+\.py$ - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.45.0 hooks: - id: markdownlint - repo: local hooks: + - id: pytest + name: "pytest" + entry: script/run-in-env.sh pytest + language: script + types: [python] + pass_filenames: false + files: ^(airos|tests|script)/.+\.py$ + - id: pylint + name: "pylinting" + entry: script/run-in-env.sh pylint -j 0 + language: script + types: [python] + files: ^(airos|tests|script)/.+\.py$ - id: mypy name: mypy entry: script/run-in-env.sh mypy language: script require_serial: true types_or: [python, pyi] - files: ^(airos|tests|scripts)/.+\.(py|pyi)$ + files: ^(airos|tests|script)/.+\.(py|pyi)$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d2a8e..081c9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [0.2.10] - 2025-08-13 + +### Changed + +- Maintenance chores +- Added pylint and pytest (and applicable changes) + ## [0.2.9] - 2025-08-12 ### Changed diff --git a/airos/airos8.py b/airos/airos8.py index c8a35cc..6afbd8e 100644 --- a/airos/airos8.py +++ b/airos/airos8.py @@ -193,7 +193,7 @@ async def login(self) -> bool: _LOGGER.info("Login task was cancelled") raise - def derived_data(self, response: dict[str, Any] = {}) -> dict[str, Any]: + def derived_data(self, response: dict[str, Any]) -> dict[str, Any]: """Add derived data to the device response.""" derived: dict[str, Any] = { "station": False, diff --git a/pyproject.toml b/pyproject.toml index 5283b42..99f9b9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.2.9" +version = "0.2.10" license = "MIT" description = "Ubiquity airOS module(s) for Python 3." readme = "README.md" diff --git a/script/generate_ha_fixture.py b/script/generate_ha_fixture.py index d189b64..60276de 100644 --- a/script/generate_ha_fixture.py +++ b/script/generate_ha_fixture.py @@ -42,13 +42,13 @@ def generate_airos_fixtures() -> None: _LOGGER.info("Processing '%s'...", filename) try: - with open(base_fixture_path) as source: + with open(base_fixture_path, encoding="utf-8") as source: source_data = json.loads(source.read()) derived_data = AirOS.derived_data(None, source_data) # type: ignore[arg-type] new_data = AirOSData.from_dict(derived_data) - with open(new_fixture_path, "w") as new: + with open(new_fixture_path, "w", encoding="utf-8") as new: json.dump(new_data.to_dict(), new, indent=2, sort_keys=True) _LOGGER.info("Successfully created '%s'", new_filename) diff --git a/script/mashumaro-step-debug.py b/script/mashumaro-step-debug.py index 300c864..33f816e 100644 --- a/script/mashumaro-step-debug.py +++ b/script/mashumaro-step-debug.py @@ -6,11 +6,11 @@ import sys from typing import Any -current_script_dir = os.path.dirname(os.path.abspath(__file__)) -project_root_dir = os.path.abspath(os.path.join(current_script_dir, os.pardir)) +_current_script_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root_dir = os.path.abspath(os.path.join(_current_script_dir, os.pardir)) -if project_root_dir not in sys.path: - sys.path.append(project_root_dir) +if _project_root_dir not in sys.path: + sys.path.append(_project_root_dir) from airos.data import AirOS8Data, Interface, Remote, Station, Wireless # noqa: E402 @@ -31,7 +31,7 @@ def main() -> None: sys.path.append(project_root_dir) # Load the JSON data - with open(sys.argv[1]) as f: + with open(sys.argv[1], encoding="utf-8") as f: data = json.loads(f.read()) try: @@ -65,7 +65,6 @@ def main() -> None: _LOGGER.info(" -> Checking list of Interface objects...") interfaces = data["interfaces"] - interface_obj_list = [] # noqa: F841 for i, interface_data in enumerate(interfaces): _LOGGER.info(" -> Checking Interface object at index %s...", i) _LOGGER.info(" Interface should be %s.", interface_data["ifname"]) diff --git a/tests/conftest.py b/tests/conftest.py index ae280a5..4bf3e1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,8 @@ import aiohttp +# pylint: disable=redefined-outer-name, unnecessary-default-type-args + @pytest.fixture def base_url() -> str: diff --git a/tests/test_discovery.py b/tests/test_discovery.py index d550fab..9998b4e 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -15,6 +15,8 @@ from airos.exceptions import AirOSDiscoveryError, AirOSEndpointError, AirOSListenerError import pytest +# pylint: disable=redefined-outer-name + # Helper to load binary fixture async def _read_binary_fixture(fixture_name: str) -> bytes: From 3bcfb57d2e6008139a615e4dd537692826155b66 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 13 Aug 2025 20:04:13 +0200 Subject: [PATCH 4/4] Add pylint --- requirements-test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-test.txt b/requirements-test.txt index 7e121b0..7312432 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -9,3 +9,4 @@ aiofiles==24.1.0 radon==6.0.1 types-aiofiles==24.1.0.20250809 mypy==1.17.1 +pylint==3.3.7