Merging upstream version 1.3.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
5b922100c9
commit
8a6a3342fc
337 changed files with 16571 additions and 4891 deletions
238
asynceapi/_models.py
Normal file
238
asynceapi/_models.py
Normal file
|
@ -0,0 +1,238 @@
|
|||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
"""Models for the asynceapi package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from logging import getLogger
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from ._constants import EapiCommandFormat
|
||||
from ._errors import EapiReponseError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
from ._types import EapiComplexCommand, EapiJsonOutput, EapiSimpleCommand, EapiTextOutput, JsonRpc
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
@dataclass(frozen=True)
|
||||
class EapiRequest:
|
||||
"""Model for an eAPI request.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
commands : list[EapiSimpleCommand | EapiComplexCommand]
|
||||
A list of commands to execute.
|
||||
version : int | Literal["latest"]
|
||||
The eAPI version to use. Defaults to "latest".
|
||||
format : EapiCommandFormat
|
||||
The command output format. Defaults "json".
|
||||
timestamps : bool
|
||||
Include timestamps in the command output. Defaults to False.
|
||||
auto_complete : bool
|
||||
Enable command auto-completion. Defaults to False.
|
||||
expand_aliases : bool
|
||||
Expand command aliases. Defaults to False.
|
||||
stop_on_error : bool
|
||||
Stop command execution on first error. Defaults to True.
|
||||
id : int | str
|
||||
The request ID. Defaults to a random hex string.
|
||||
"""
|
||||
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand]
|
||||
version: int | Literal["latest"] = "latest"
|
||||
format: EapiCommandFormat = EapiCommandFormat.JSON
|
||||
timestamps: bool = False
|
||||
auto_complete: bool = False
|
||||
expand_aliases: bool = False
|
||||
stop_on_error: bool = True
|
||||
id: int | str = field(default_factory=lambda: uuid4().hex)
|
||||
|
||||
def to_jsonrpc(self) -> JsonRpc:
|
||||
"""Return the JSON-RPC dictionary payload for the request."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "runCmds",
|
||||
"params": {
|
||||
"version": self.version,
|
||||
"cmds": self.commands,
|
||||
"format": self.format,
|
||||
"timestamps": self.timestamps,
|
||||
"autoComplete": self.auto_complete,
|
||||
"expandAliases": self.expand_aliases,
|
||||
"stopOnError": self.stop_on_error,
|
||||
},
|
||||
"id": self.id,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EapiResponse:
|
||||
"""Model for an eAPI response.
|
||||
|
||||
Construct an EapiResponse from a JSON-RPC response dictionary using the `from_jsonrpc` class method.
|
||||
|
||||
Can be iterated over to access command results in order of execution.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
request_id : str
|
||||
The ID of the original request this response corresponds to.
|
||||
_results : dict[int, EapiCommandResult]
|
||||
Dictionary mapping request command indices to their respective results.
|
||||
error_code : int | None
|
||||
The JSON-RPC error code, if any.
|
||||
error_message : str | None
|
||||
The JSON-RPC error message, if any.
|
||||
"""
|
||||
|
||||
request_id: str
|
||||
_results: dict[int, EapiCommandResult] = field(default_factory=dict)
|
||||
error_code: int | None = None
|
||||
error_message: str | None = None
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
"""Return True if the response has no errors."""
|
||||
return self.error_code is None
|
||||
|
||||
@property
|
||||
def results(self) -> list[EapiCommandResult]:
|
||||
"""Get all results as a list. Results are ordered by the command indices in the request."""
|
||||
return list(self._results.values())
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of results."""
|
||||
return len(self._results)
|
||||
|
||||
def __iter__(self) -> Iterator[EapiCommandResult]:
|
||||
"""Enable iteration over the results. Results are yielded in the same order as provided in the request."""
|
||||
yield from self._results.values()
|
||||
|
||||
@classmethod
|
||||
def from_jsonrpc(cls, response: dict[str, Any], request: EapiRequest, *, raise_on_error: bool = False) -> EapiResponse:
|
||||
"""Build an EapiResponse from a JSON-RPC eAPI response.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
response
|
||||
The JSON-RPC eAPI response dictionary.
|
||||
request
|
||||
The corresponding EapiRequest.
|
||||
raise_on_error
|
||||
Raise an EapiReponseError if the response contains errors, by default False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
EapiResponse
|
||||
The EapiResponse object.
|
||||
"""
|
||||
has_error = "error" in response
|
||||
response_data = response["error"]["data"] if has_error else response["result"]
|
||||
|
||||
# Handle case where we have fewer results than commands (stop_on_error=True)
|
||||
executed_count = min(len(response_data), len(request.commands))
|
||||
|
||||
# Process the results we have
|
||||
results = {}
|
||||
for i in range(executed_count):
|
||||
cmd = request.commands[i]
|
||||
cmd_str = cmd["cmd"] if isinstance(cmd, dict) else cmd
|
||||
data = response_data[i]
|
||||
|
||||
output = None
|
||||
errors = []
|
||||
success = True
|
||||
start_time = None
|
||||
duration = None
|
||||
|
||||
# Parse the output based on the data type, no output when errors are present
|
||||
if isinstance(data, dict):
|
||||
if "errors" in data:
|
||||
errors = data["errors"]
|
||||
success = False
|
||||
else:
|
||||
output = data["output"] if request.format == EapiCommandFormat.TEXT and "output" in data else data
|
||||
|
||||
# Add timestamps if available
|
||||
if request.timestamps and "_meta" in data:
|
||||
meta = data.pop("_meta")
|
||||
start_time = meta.get("execStartTime")
|
||||
duration = meta.get("execDuration")
|
||||
|
||||
elif isinstance(data, str):
|
||||
# Handle case where eAPI returns a JSON string response (serialized JSON) for certain commands
|
||||
try:
|
||||
from json import JSONDecodeError, loads
|
||||
|
||||
output = loads(data)
|
||||
except (JSONDecodeError, TypeError):
|
||||
# If it's not valid JSON, store as is
|
||||
LOGGER.warning("Invalid JSON response for command: %s. Storing as text: %s", cmd_str, data)
|
||||
output = data
|
||||
|
||||
results[i] = EapiCommandResult(
|
||||
command=cmd_str,
|
||||
output=output,
|
||||
errors=errors,
|
||||
success=success,
|
||||
start_time=start_time,
|
||||
duration=duration,
|
||||
)
|
||||
|
||||
# If stop_on_error is True and we have an error, indicate commands not executed
|
||||
if has_error and request.stop_on_error and executed_count < len(request.commands):
|
||||
for i in range(executed_count, len(request.commands)):
|
||||
cmd = request.commands[i]
|
||||
cmd_str = cmd["cmd"] if isinstance(cmd, dict) else cmd
|
||||
results[i] = EapiCommandResult(command=cmd_str, output=None, errors=["Command not executed due to previous error"], success=False, executed=False)
|
||||
|
||||
response_obj = cls(
|
||||
request_id=response["id"],
|
||||
_results=results,
|
||||
error_code=response["error"]["code"] if has_error else None,
|
||||
error_message=response["error"]["message"] if has_error else None,
|
||||
)
|
||||
|
||||
if raise_on_error and has_error:
|
||||
raise EapiReponseError(response_obj)
|
||||
|
||||
return response_obj
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EapiCommandResult:
|
||||
"""Model for an eAPI command result.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
command : str
|
||||
The command that was executed.
|
||||
output : EapiJsonOutput | EapiTextOutput | None
|
||||
The command result output. None if the command returned errors.
|
||||
errors : list[str]
|
||||
A list of error messages, if any.
|
||||
success : bool
|
||||
True if the command was successful.
|
||||
executed : bool
|
||||
True if the command was executed. When `stop_on_error` is True in the request, some commands may not be executed.
|
||||
start_time : float | None
|
||||
Command execution start time in seconds. Uses Unix epoch format. `timestamps` must be True in the request.
|
||||
duration : float | None
|
||||
Command execution duration in seconds. `timestamps` must be True in the request.
|
||||
"""
|
||||
|
||||
command: str
|
||||
output: EapiJsonOutput | EapiTextOutput | None
|
||||
errors: list[str] = field(default_factory=list)
|
||||
success: bool = True
|
||||
executed: bool = True
|
||||
start_time: float | None = None
|
||||
duration: float | None = None
|
Loading…
Add table
Add a link
Reference in a new issue