Resolve various values of JsonAPICall in case they contain context variables eg. endpoint, query etc.

Update tests.
pull/3560/head
derekpierre 2024-10-25 13:30:21 -04:00
parent b5e35a7188
commit 788eb6f3ec
No known key found for this signature in database
3 changed files with 86 additions and 31 deletions

View File

@ -93,6 +93,13 @@ def is_context_variable(variable) -> bool:
return isinstance(variable, str) and CONTEXT_REGEX.fullmatch(variable)
def string_contains_context_variable(variable: str) -> bool:
matches = re.findall(CONTEXT_REGEX, variable)
if not matches:
return False
return True
def get_context_value(context_variable: str, **context) -> Any:
try:
# DIRECTIVES are special context vars that will pre-processed by ursula

View File

@ -10,7 +10,8 @@ from marshmallow.validate import OneOf
from nucypher.policy.conditions.base import ExecutionCall
from nucypher.policy.conditions.context import (
is_context_variable,
resolve_context_variable,
resolve_any_context_variables,
string_contains_context_variable,
)
from nucypher.policy.conditions.exceptions import (
ConditionEvaluationFailed,
@ -34,7 +35,8 @@ class JSONPathField(Field):
if not isinstance(value, str):
raise self.make_error("invalidType", value=type(value))
try:
parse(value)
if not string_contains_context_variable(value):
parse(value)
except (JsonPathLexerError, JsonPathParserError):
raise self.make_error("invalid", value=value)
return value
@ -84,35 +86,37 @@ class JsonApiCall(ExecutionCall):
super().__init__()
def execute(self, **context) -> Any:
resolved_authorization_token = None
if self.authorization_token:
resolved_authorization_token = resolve_context_variable(
self.authorization_token, **context
)
response = self._fetch(resolved_authorization_token)
response = self._fetch(**context)
data = self._deserialize_response(response)
result = self._query_response(data)
result = self._query_response(data, **context)
return result
def _fetch(self, authorization_token: str = None) -> requests.Response:
def _fetch(self, **context) -> requests.Response:
"""Fetches data from the endpoint."""
resolved_endpoint = resolve_any_context_variables(self.endpoint, **context)
resolved_inputs = resolve_any_context_variables(self.inputs, **context)
headers = None
if self.authorization_token:
resolved_authorization_token = resolve_any_context_variables(
self.authorization_token, **context
)
headers = {"Authorization": f"Bearer {resolved_authorization_token}"}
try:
headers = None
if authorization_token:
headers = {"Authorization": f"Bearer {authorization_token}"}
if self.method == self.HTTP_METHOD_GET:
response = requests.get(
self.endpoint,
params=self.inputs,
resolved_endpoint,
params=resolved_inputs,
timeout=self.timeout,
headers=headers,
)
else:
response = requests.post(
self.endpoint,
data=self.inputs,
resolved_endpoint,
data=resolved_inputs,
timeout=self.timeout,
headers=headers,
)
@ -121,20 +125,20 @@ class JsonApiCall(ExecutionCall):
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}"
f"Failed to fetch endpoint {resolved_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}"
f"Failed to fetch endpoint {resolved_endpoint}: {request_error}"
)
if response.status_code != 200:
self.logger.error(
f"Failed to fetch endpoint {self.endpoint}: {response.status_code}"
f"Failed to fetch endpoint {resolved_endpoint}: {response.status_code}"
)
raise ConditionEvaluationFailed(
f"Failed to fetch endpoint {self.endpoint}: {response.status_code}"
f"Failed to fetch endpoint {resolved_endpoint}: {response.status_code}"
)
return response
@ -150,16 +154,18 @@ class JsonApiCall(ExecutionCall):
)
return data
def _query_response(self, data: Any) -> Any:
def _query_response(self, data: Any, **context) -> Any:
if not self.query:
return data # primitive value
resolved_query = resolve_any_context_variables(self.query, **context)
try:
expression = parse(self.query)
expression = parse(resolved_query)
matches = expression.find(data)
if not matches:
message = f"No matches found for the JSONPath query: {self.query}"
message = f"No matches found for the JSONPath query: {resolved_query}"
self.logger.info(message)
raise ConditionEvaluationFailed(message)
except (JsonPathLexerError, JsonPathParserError) as jsonpath_err:
@ -167,9 +173,7 @@ class JsonApiCall(ExecutionCall):
raise ConditionEvaluationFailed(f"JSONPath error: {jsonpath_err}")
if len(matches) > 1:
message = (
f"Ambiguous JSONPath query - Multiple matches found for: {self.query}"
)
message = f"Ambiguous JSONPath query - Multiple matches found for: {resolved_query}"
self.logger.info(message)
raise ConditionEvaluationFailed(message)
result = matches[0].value

View File

@ -255,7 +255,7 @@ def test_basic_json_api_condition_evaluation_with_parameters(mocker):
assert mocked_get.call_count == 1
def test_basic_json_api_condition_evaluation_with_auth_token(mocker):
def test_json_api_condition_evaluation_with_auth_token(mocker):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(
@ -286,6 +286,49 @@ def test_basic_json_api_condition_evaluation_with_auth_token(mocker):
)
def test_json_api_condition_evaluation_with_various_context_variables(mocker):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(
status_code=200, json=lambda: {"ethereum": {"cad": 0.0}}
),
)
condition = JsonApiCondition(
endpoint="https://api.coingecko.com/api/:version/simple/:endpointPath",
method=JsonApiCall.HTTP_METHOD_GET,
inputs={
"ids": "ethereum",
"vs_currencies": ":vsCurrency",
},
authorization_token=":authToken",
query="ethereum.:vsCurrency",
return_value_test=ReturnValueTest("==", ":expectedPrice"),
)
assert condition.authorization_token == ":authToken"
auth_token = "1234567890"
context = {
":endpointPath": "price",
":version": "v3",
":vsCurrency": "cad",
":authToken": f"{auth_token}",
":expectedPrice": 0.0,
}
assert condition.verify(**context) == (True, 0.0)
assert mocked_get.call_count == 1
call_args = mocked_get.call_args
assert call_args.args == (
f"https://api.coingecko.com/api/{context[':version']}/simple/{context[':endpointPath']}",
)
assert call_args.kwargs["headers"]["Authorization"] == f"Bearer {auth_token}"
assert call_args.kwargs["params"] == {
"ids": "ethereum",
"vs_currencies": context[":vsCurrency"],
}
def test_json_api_condition_from_lingo_expression():
lingo_dict = {
"conditionType": "json-api",
@ -298,7 +341,7 @@ def test_json_api_condition_from_lingo_expression():
"query": "$.store.book[0].price",
"returnValueTest": {
"comparator": "==",
"value": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"value": 1.0,
},
}
@ -324,7 +367,7 @@ def test_json_api_condition_from_lingo_expression_with_authorization():
"query": "$.store.book[0].price",
"returnValueTest": {
"comparator": "==",
"value": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"value": 1.0,
},
}
@ -336,6 +379,7 @@ def test_json_api_condition_from_lingo_expression_with_authorization():
assert isinstance(condition, JsonApiCondition)
assert condition.to_dict() == lingo_dict
def test_ambiguous_json_path_multiple_results(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"price": 1}, {"price": 2}]}}