Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
- name: Run all tests
run: |
. venv/bin/activate
pytest --log-level info tests/*.py --cov='.'
pytest --log-level info tests/ --cov='airos/'
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ repos:
pass_filenames: false
- id: pytest
name: "pytest"
entry: script/run-in-env.sh pytest
entry: script/run-in-env.sh pytest --log-level info tests/ --cov='airos/'
language: script
types: [python]
pass_filenames: false
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [0.4.2] - 2025-08-17

### Changed

- Aligned quality targets either improved or tagged

## [0.4.1] - 2025-08-17

### Changed
Expand Down
5 changes: 2 additions & 3 deletions airos/airos8.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async def _request_json(
raise AirOSConnectionAuthenticationError from err
raise AirOSConnectionSetupError from err
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.exception("Error during API call to %s: %s", url, err)
_LOGGER.exception("Error during API call to %s", url)
raise AirOSDeviceConnectionError from err
except json.JSONDecodeError as err:
_LOGGER.error("Failed to decode JSON from %s", url)
Expand All @@ -242,8 +242,7 @@ async def status(self) -> AirOSData:

try:
adjusted_json = self.derived_data(response)
airos_data = AirOSData.from_dict(adjusted_json)
return airos_data
return AirOSData.from_dict(adjusted_json)
except InvalidFieldValue as err:
# Log with .error() as this is a specific, known type of issue
redacted_data = redact_data_smart(response)
Expand Down
8 changes: 1 addition & 7 deletions airos/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ def is_ip_address(value: str) -> bool:
"""Check if a string is a valid IPv4 or IPv6 address."""
try:
ipaddress.ip_address(value)
return True
except ValueError:
return False
return True


def redact_data_smart(data: dict[str, Any]) -> dict[str, Any]:
Expand Down Expand Up @@ -107,8 +107,6 @@ def _redact(d: dict[str, Any]) -> dict[str, Any]:
class AirOSDataClass(DataClassDictMixin):
"""A base class for all mashumaro dataclasses."""

pass


def _check_and_log_unknown_enum_value(
data_dict: dict[str, Any],
Expand Down Expand Up @@ -534,15 +532,11 @@ class Interface(AirOSDataClass):
class ProvisioningMode(AirOSDataClass):
"""Leaf definition."""

pass


@dataclass
class NtpClient(AirOSDataClass):
"""Leaf definition."""

pass


@dataclass
class GPSMain(AirOSDataClass):
Expand Down
16 changes: 10 additions & 6 deletions airos/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
log = f"Truncated MAC address TLV (Type 0x06). Expected {expected_length}, got {len(data) - offset} bytes. Remaining: {data[offset:].hex()}"
_LOGGER.warning(log)
log = f"Malformed packet: {log}"
raise AirOSEndpointError(log)
raise AirOSEndpointError(log) from None # noqa: TRY301

elif tlv_type in [
0x02,
Expand All @@ -169,7 +169,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
log = f"Truncated TLV (Type {tlv_type:#x}), no 2-byte length field. Remaining: {data[offset:].hex()}"
_LOGGER.warning(log)
log = f"Malformed packet: {log}"
raise AirOSEndpointError(log)
raise AirOSEndpointError(log) from None # noqa: TRY301

tlv_length: int = struct.unpack_from(">H", data, offset)[0]
offset += 2
Expand All @@ -181,7 +181,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
f"Data from TLV start: {data[offset - 3 :].hex()}"
)
_LOGGER.warning(log)
raise AirOSEndpointError(f"Malformed packet: {log}")
raise AirOSEndpointError(f"Malformed packet: {log}") from None # noqa: TRY301

tlv_value: bytes = data[offset : offset + tlv_length]

Expand All @@ -194,7 +194,9 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
else:
log = f"Unexpected length for 0x02 TLV (MAC+IP). Expected 10, got {tlv_length}. Value: {tlv_value.hex()}"
_LOGGER.warning(log)
raise AirOSEndpointError(f"Malformed packet: {log}")
raise AirOSEndpointError( # noqa: TRY301
f"Malformed packet: {log}"
) from None

elif tlv_type == 0x03:
parsed_info["firmware_version"] = tlv_value.decode(
Expand All @@ -213,7 +215,9 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
else:
log = f"Unexpected length for Uptime (Type 0x0A): {tlv_length}. Value: {tlv_value.hex()}"
_LOGGER.warning(log)
raise AirOSEndpointError(f"Malformed packet: {log}")
raise AirOSEndpointError( # noqa: TRY301
f"Malformed packet: {log}"
) from None

elif tlv_type == 0x0B:
parsed_info["hostname"] = tlv_value.decode(
Expand Down Expand Up @@ -260,7 +264,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
log += f"Cannot determine length, stopping parsing. Remaining: {data[offset - 1 :].hex()}"
_LOGGER.warning(log)
log = f"Malformed packet: {log}"
raise AirOSEndpointError(log)
raise AirOSEndpointError(log) from None # noqa: TRY301

except (struct.error, IndexError) as err:
log = f"Parsing error (struct/index) in AirOSDiscoveryProtocol: {err} at offset {offset}. Remaining data: {data[offset:].hex()}"
Expand Down
44 changes: 44 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[mypy]
python_version = 3.13
platform = linux
plugins = pydantic.mypy, pydantic.v1.mypy
show_error_codes = true
follow_imports = normal
local_partial_types = true
strict_equality = true
strict_bytes = true
no_implicit_optional = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = deprecated, ignore-without-code, redundant-self, truthy-iterable
disable_error_code = annotation-unchecked, import-not-found, import-untyped
extra_checks = false
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true

[pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true

[mypy-airos.*]
no_implicit_reexport = true

[mypy-tests.*]
check_untyped_defs = false
disallow_incomplete_defs = false
disallow_subclassing_any = false
disallow_untyped_calls = false
disallow_untyped_decorators = false
disallow_untyped_defs = false
warn_return_any = false
warn_unreachable = false
Loading