mirror of https://github.com/nucypher/nucypher.git
Resolve various values of JsonAPICall in case they contain context variables eg. endpoint, query etc.
Update tests.pull/3560/head
parent
b5e35a7188
commit
788eb6f3ec
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}]}}
|
||||
|
|
Loading…
Reference in New Issue