mirror of https://github.com/nucypher/nucypher.git
error handling and logging for offchain condition evaluations
parent
0fe3a9b5f1
commit
ecc8b16602
|
@ -7,9 +7,13 @@ from marshmallow import ValidationError, fields, post_load, validate
|
|||
from marshmallow.fields import Field
|
||||
|
||||
from nucypher.policy.conditions.base import AccessControlCondition
|
||||
from nucypher.policy.conditions.exceptions import InvalidCondition
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
ConditionEvaluationFailed,
|
||||
InvalidCondition,
|
||||
)
|
||||
from nucypher.policy.conditions.lingo import ConditionType, ReturnValueTest
|
||||
from nucypher.policy.conditions.utils import CamelCaseSchema
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
||||
class JSONPathField(Field):
|
||||
|
@ -33,6 +37,7 @@ class OffchainCondition(AccessControlCondition):
|
|||
"""
|
||||
|
||||
CONDITION_TYPE = ConditionType.OFFCHAIN.value
|
||||
LOGGER = Logger("nucypher.policy.conditions.offchain")
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
|
||||
|
@ -62,8 +67,6 @@ class OffchainCondition(AccessControlCondition):
|
|||
parameters: Optional[dict] = None,
|
||||
condition_type: str = ConditionType.OFFCHAIN.value,
|
||||
):
|
||||
|
||||
# internal
|
||||
if condition_type != self.CONDITION_TYPE:
|
||||
raise InvalidCondition(
|
||||
f"{self.__class__.__name__} must be instantiated with the {self.CONDITION_TYPE} type."
|
||||
|
@ -74,33 +77,61 @@ class OffchainCondition(AccessControlCondition):
|
|||
self.parameters = parameters
|
||||
self.query = query
|
||||
self.return_value_test = return_value_test
|
||||
self.logger = self.LOGGER
|
||||
|
||||
def fetch(self):
|
||||
def fetch(self) -> requests.Response:
|
||||
"""Fetches data from the endpoint."""
|
||||
try:
|
||||
response = requests.get(
|
||||
self.endpoint, params=self.parameters, headers=self.headers
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise InvalidCondition(f"Failed to fetch endpoint {self.endpoint}: {e}")
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as http_error:
|
||||
self.logger.error(f"HTTP error occurred: {http_error}")
|
||||
raise ConditionEvaluationFailed(
|
||||
f"Failed to fetch endpoint {self.endpoint}: {http_error}"
|
||||
)
|
||||
except requests.exceptions.RequestException as request_error:
|
||||
self.logger.error(f"Request exception occurred: {request_error}")
|
||||
raise InvalidCondition(
|
||||
f"Failed to fetch endpoint {self.endpoint}: {request_error}"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def deserialize_response(self, response: requests.Response) -> Any:
|
||||
"""Deserializes the JSON response from the endpoint."""
|
||||
try:
|
||||
data = response.json()
|
||||
except requests.exceptions.RequestException as json_error:
|
||||
self.logger.error(f"JSON parsing error occurred: {json_error}")
|
||||
raise ConditionEvaluationFailed(
|
||||
f"Failed to parse JSON response: {json_error}"
|
||||
)
|
||||
return data
|
||||
|
||||
def query_response(self, data: Any) -> Any:
|
||||
try:
|
||||
expression = parse(self.query)
|
||||
matches = expression.find(data)
|
||||
if not matches:
|
||||
self.logger.info("No matches found for the JSONPath query.")
|
||||
raise ConditionEvaluationFailed(
|
||||
"No matches found for the JSONPath query."
|
||||
)
|
||||
result = matches[0].value
|
||||
except (JsonPathLexerError, JsonPathParserError) as jsonpath_err:
|
||||
self.logger.error(f"JSONPath error occurred: {jsonpath_err}")
|
||||
raise ConditionEvaluationFailed(f"JSONPath error: {jsonpath_err}")
|
||||
return result
|
||||
|
||||
def verify(self, **context) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Verifies the offchain condition is met by performing a read operation on the endpoint
|
||||
and evaluating the return value test with the result. Parses the endpoint's JSON response using
|
||||
jsonpath.
|
||||
and evaluating the return value test with the result. Parses the endpoint's JSON response using
|
||||
JSONPath.
|
||||
"""
|
||||
|
||||
response = self.fetch()
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise InvalidCondition(f"Failed to parse JSON response: {e}")
|
||||
|
||||
expression = parse(self.query)
|
||||
|
||||
# TODO: Uses the first match, is it beneficial to support multiple matches?
|
||||
result = expression.find(data)[0].value
|
||||
|
||||
data = self.deserialize_response(response)
|
||||
result = self.query_response(data)
|
||||
return self.return_value_test.eval(result), result
|
||||
|
|
|
@ -2,7 +2,10 @@ import pytest
|
|||
import requests
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from nucypher.policy.conditions.exceptions import InvalidCondition
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
ConditionEvaluationFailed,
|
||||
InvalidCondition,
|
||||
)
|
||||
from nucypher.policy.conditions.lingo import ReturnValueTest
|
||||
from nucypher.policy.conditions.offchain import JSONPathField, OffchainCondition
|
||||
|
||||
|
@ -149,6 +152,6 @@ def test_offchain_condition_verify_invalid_json(mocker):
|
|||
query="$.store.book[0].price",
|
||||
return_value_test=ReturnValueTest("==", "2"),
|
||||
)
|
||||
with pytest.raises(InvalidCondition) as excinfo:
|
||||
with pytest.raises(ConditionEvaluationFailed) as excinfo:
|
||||
condition.verify()
|
||||
assert "Failed to parse JSON response" in str(excinfo.value)
|
||||
|
|
Loading…
Reference in New Issue