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
21 changes: 5 additions & 16 deletions aiopnsense/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,11 @@ async def _run_validation_request(self, request: Callable[[], Awaitable[_T]]) ->
used during client validation.

Returns:
_T: Result returned by the validation request.
_T: Decoded response from the successful validation request.

Raises:
OPNsenseInvalidURL: Raised when the configured URL is invalid.
OPNsenseSSLError: Raised when the TLS handshake fails.
OPNsenseTimeoutError: Raised when validation requests time out.
OPNsenseInvalidAuth: Raised when API authentication fails.
OPNsensePrivilegeMissing: Raised when the API user lacks privileges.
OPNsenseConnectionError: Raised when another client connection error occurs.
_map_opnsense_exception: Raised as the mapped public OPNsense error
when the request encounters an aiohttp client error or times out.
"""
try:
return await request()
Expand All @@ -101,24 +97,17 @@ async def _run_validation_request(self, request: Callable[[], Awaitable[_T]]) ->

async def validate(self, *, require_device_id: bool = True) -> None:
"""Validate connectivity, authentication, and minimum firmware support.
Note that this will throw errors, regardless of what self._throw_errors is set to.

This raises request failures regardless of ``self._throw_errors``; those
failures are mapped to public OPNsense errors by ``_run_validation_request``.

Args:
require_device_id (bool): Whether validation must resolve a physical-device
unique ID.

Raises:
OPNsenseInvalidURL: Raised when the configured URL is invalid.
OPNsenseSSLError: Raised when the TLS handshake fails.
OPNsenseTimeoutError: Raised when validation requests time out.
OPNsenseInvalidAuth: Raised when API authentication fails.
OPNsensePrivilegeMissing: Raised when the API user lacks privileges.
OPNsenseConnectionError: Raised when another client connection error occurs.
OPNsenseUnknownFirmware: Raised when firmware detection returns no version.
OPNsenseBelowMinFirmware: Raised when the detected firmware is unsupported.
OPNsenseMissingDeviceUniqueID: Raised when no device unique ID can be
resolved and `require_device_id` is `True`.
"""
orig_throw_errors = self._throw_errors
self._throw_errors = True
Expand Down
4 changes: 2 additions & 2 deletions aiopnsense/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ def __init__(
username (str): Username for API authentication.
password (str): Password for API authentication.
session (aiohttp.ClientSession): HTTP client session used for API requests.
opts (MutableMapping[str, Any] | None, optional): Optional client configuration values
opts (MutableMapping[str, Any] | None): Optional client configuration values
(e.g. ``opts={"verify_ssl": True}``).
initial (bool | object): Deprecated alias for ``throw_errors``. When provided,
a ``DeprecationWarning`` is emitted. Ignored when ``throw_errors`` is also set.
name (str): Display name for the client instance.
throw_errors (bool | object): Whether request and decorator errors should be
re-raised instead of logged and suppressed. Defaults to ``False``.
name (str): Display name for the client instance.

Raises:
OPNsenseInvalidArgument: Raised when ``initial`` or ``throw_errors`` is not a ``bool``.
Expand Down
13 changes: 10 additions & 3 deletions aiopnsense/client_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ def _is_post_endpoint_probe_blocked(self, path: str) -> bool:

@deprecated("Endpoint style selection is internal. Direct calls are no longer needed.")
async def set_use_snake_case(self, initial: bool = False) -> None:
"""Deprecated wrapper that preserves legacy ``initial`` compatibility."""
"""Deprecated wrapper that preserves legacy ``initial`` compatibility.

Args:
initial (bool): Whether to preserve legacy initial-detection behavior.
"""
await self._set_use_snake_case(initial=initial)

async def _set_use_snake_case(self, initial: bool = False) -> None:
Expand Down Expand Up @@ -208,8 +212,11 @@ async def _is_endpoint_available(
When no exception is raised, this method always returns a ``bool``.

Raises:
OPNsenseError: Raised when an HTTP response or transport error occurs and
``self._throw_errors`` is ``True``.
_opnsense_http_error: Raised as the mapped public OPNsense error for a
non-successful HTTP response when ``self._throw_errors`` is ``True``.
_map_opnsense_exception: Raised as the mapped public OPNsense error for
an aiohttp client error or timeout when ``self._throw_errors`` is
``True``.

Side Effects:
Increments the REST query counter for uncached probes and updates
Expand Down
48 changes: 44 additions & 4 deletions aiopnsense/client_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,34 @@ async def _do_get(
*,
response_format: Literal["json", "text"] = "json",
) -> MutableMapping[str, Any] | list | str | None:
"""Execute a queued GET request."""
"""Execute an immediate GET transport request after it is dequeued.

Args:
path (str): API endpoint path to request.
caller (str): Calling method name used for diagnostics.
timeout_seconds (float | None): Optional transport request timeout.
response_format (Literal['json', 'text']): Expected response body format.

Returns:
MutableMapping[str, Any] | list | str | None: Decoded transport
response, if available.
"""
...

async def _do_get_from_stream(
self,
path: str,
caller: str = "Unknown",
) -> dict[str, Any]:
"""Execute a queued streaming GET request."""
"""Execute an immediate streaming GET transport request after dequeuing it.

Args:
path (str): Streaming API endpoint path to request.
caller (str): Calling method name used for diagnostics.

Returns:
dict[str, Any]: Decoded payload extracted from the stream response.
"""
...

async def _do_post(
Expand All @@ -43,7 +62,17 @@ async def _do_post(
payload: MutableMapping[str, Any] | None = None,
caller: str = "Unknown",
) -> MutableMapping[str, Any] | list | None:
"""Execute a queued POST request."""
"""Execute an immediate POST transport request after it is dequeued.

Args:
path (str): API endpoint path to request.
payload (MutableMapping[str, Any] | None): Optional request body.
caller (str): Calling method name used for diagnostics.

Returns:
MutableMapping[str, Any] | list | None: Decoded transport response,
if available.
"""
...

async def _ensure_workers_started(self) -> None:
Expand All @@ -60,6 +89,10 @@ async def _get_active_loop(self) -> asyncio.AbstractEventLoop:
Returns:
asyncio.AbstractEventLoop: Running event loop used to create
queued request futures.

Raises:
OPNsenseError: Raised when worker startup does not initialize an event
loop.
"""
await self._ensure_workers_started()
if self._loop is None:
Expand Down Expand Up @@ -131,6 +164,9 @@ async def _get_text(self, path: str) -> str | None:

Returns:
str | None: Response body text, or ``None`` when the request fails.

Raises:
OPNsenseError: Raised when the queued request returns a non-text response.
"""
result = await self._queue_request("get_text", path)
if result is None or isinstance(result, str):
Expand All @@ -152,7 +188,11 @@ async def _post(
return await self._queue_request("post", path, payload)

async def _process_queue(self) -> None:
"""Continuously process queued API requests and resolve waiting futures."""
"""Continuously process queued API requests and resolve waiting futures.

Raises:
asyncio.CancelledError: Raised when the queue processor is cancelled.
"""
while True:
method: str | None = None
path: str | None = None
Expand Down
47 changes: 45 additions & 2 deletions aiopnsense/client_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,28 @@ class ClientTransportMixin:
_verify_ssl: bool

async def _get(self, path: str) -> MutableMapping[str, Any] | list | None:
"""Queue a GET request and return the decoded payload."""
"""Queue a GET request and return the decoded payload.

Args:
path (str): API path to request.

Returns:
MutableMapping[str, Any] | list | None: Decoded response payload.
"""
...

async def _post(
self, path: str, payload: MutableMapping[str, Any] | None = None
) -> MutableMapping[str, Any] | list | None:
"""Queue a POST request and return the decoded payload."""
"""Queue a POST request and return the decoded payload.

Args:
path (str): API path to request.
payload (MutableMapping[str, Any] | None): JSON object to send with the request.

Returns:
MutableMapping[str, Any] | list | None: Decoded response payload.
"""
...

async def _do_get_from_stream(self, path: str, caller: str = "Unknown") -> dict[str, Any]:
Expand All @@ -45,6 +60,13 @@ async def _do_get_from_stream(self, path: str, caller: str = "Unknown") -> dict[

Returns:
dict[str, Any]: Decoded payload extracted from the streaming API response.

Raises:
_opnsense_http_error: Raised as the mapped public OPNsense error for a
non-successful HTTP response when ``self._throw_errors`` is ``True``.
_map_opnsense_exception: Raised as the mapped public OPNsense error for
an aiohttp client error or timeout when ``self._throw_errors`` is
``True``.
"""
self._rest_api_query_count += 1
url: str = f"{self._url}{path}"
Expand Down Expand Up @@ -124,6 +146,13 @@ async def _stream_json_events(

Yields:
dict[str, Any]: Decoded JSON object from each valid ``data:`` event.

Raises:
_opnsense_http_error: Raised as the mapped public OPNsense error for a
non-successful HTTP response when ``self._throw_errors`` is ``True``.
_map_opnsense_exception: Raised as the mapped public OPNsense error for
an aiohttp client error or timeout when ``self._throw_errors`` is
``True``.
"""
self._rest_api_query_count += 1
url: str = f"{self._url}{path}"
Expand Down Expand Up @@ -281,6 +310,13 @@ async def _do_get(
Returns:
MutableMapping[str, Any] | list | str | None: Decoded response payload
returned by the GET request.

Raises:
_opnsense_http_error: Raised as the mapped public OPNsense error for a
non-successful HTTP response when ``self._throw_errors`` is ``True``.
_map_opnsense_exception: Raised as the mapped public OPNsense error for
an aiohttp client error or timeout when ``self._throw_errors`` is
``True``.
"""
self._rest_api_query_count += 1
url: str = f"{self._url}{path}"
Expand Down Expand Up @@ -395,6 +431,13 @@ async def _do_post(

Returns:
MutableMapping[str, Any] | list | None: Decoded response payload returned by the POST request.

Raises:
_opnsense_http_error: Raised as the mapped public OPNsense error for a
non-successful HTTP response when ``self._throw_errors`` is ``True``.
_map_opnsense_exception: Raised as the mapped public OPNsense error for
an aiohttp client error or timeout when ``self._throw_errors`` is
``True``.
"""
self._rest_api_query_count += 1
url: str = f"{self._url}{path}"
Expand Down
35 changes: 27 additions & 8 deletions aiopnsense/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,29 @@ async def inner(self: Any, *args: Any, **kwargs: Any) -> Any:
"""Execute wrapped coroutine with shared timeout/exception logging.

Args:
*args (Any): Positional arguments forwarded to the wrapped callable.
**kwargs (Any): Keyword arguments forwarded to the wrapped callable.
self (Any): Client instance whose ``_throw_errors`` setting controls
whether caught errors are mapped and propagated or logged and
suppressed.
args (Any): Positional arguments forwarded to the wrapped coroutine.
kwargs (Any): Keyword arguments forwarded to the wrapped coroutine.

Returns:
Any: Value produced by the wrapped callable, or ``None`` when an
error is suppressed.
Any: Wrapped coroutine result, or ``None`` when an error is
suppressed.

Raises:
asyncio.CancelledError: Re-raised when the wrapped coroutine is
cancelled.
OPNsenseTimeoutError: Re-raised when ``_throw_errors`` is true
and the wrapped coroutine raises this OPNsense error.
TimeoutError: Caught and mapped to an OPNsense error when
``_throw_errors`` is true.
aiohttp.ServerTimeoutError: Caught and mapped to an OPNsense error
when ``_throw_errors`` is true.
Exception: Re-raised or mapped to an OPNsense error when
``_throw_errors`` is true.
_map_opnsense_exception: Maps caught errors to an OPNsense error
when ``_throw_errors`` is true.
"""
try:
return await func(self, *args, **kwargs)
Expand Down Expand Up @@ -392,14 +409,16 @@ def normalize_lookup_token(value: Any) -> str:


def api_value_matches(value: object, expected: str) -> bool:
"""Compare OPNsense API values across string, numeric, and boolean forms.
"""Compare a normalized OPNsense API value with its expected string.

Args:
value: Raw value returned by OPNsense APIs.
expected: Normalized expected value.
value (object): API value to normalize; booleans become integer strings and
all other values are converted directly to strings.
expected (str): Expected normalized string value.

Returns:
``True`` when the normalized API value matches ``expected``.
bool: ``True`` when the normalized API value has the same string representation
as ``expected``.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"""
if isinstance(value, bool):
value = int(value)
Expand Down
4 changes: 0 additions & 4 deletions aiopnsense/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,6 @@ async def get_device_unique_id(self, expected_id: str | None = None) -> str | No
physical interface, otherwise the first sorted physical MAC
identifier. Returns ``None`` when no physical MAC addresses are
available and ``throw_errors`` is disabled.

Raises:
OPNsenseMissingDeviceUniqueID: No device unique ID could be
resolved and ``throw_errors`` is enabled.
"""
if not await self._is_get_endpoint_available(INTERFACE_OVERVIEW_EXPORT_ENDPOINT):
_LOGGER.debug("Interface overview endpoint unavailable for device id resolution")
Expand Down
Loading
Loading