respond to RFCs in PR #3511

pull/3554/head
KPrasch 2024-06-28 17:27:21 +08:00 committed by derekpierre
parent f59d636d54
commit f2c7337483
No known key found for this signature in database
4 changed files with 54 additions and 164 deletions

113
Pipfile
View File

@ -1,113 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
abnf = "==1.1.1"
aiohttp = "==3.9.4rc0"
aiosignal = "==1.3.1"
annotated-types = "==0.6.0"
appdirs = "==1.4.4"
async-timeout = "==4.0.3"
attrs = "==23.2.0"
atxm = "==0.3.0"
autobahn = "==23.1.2"
automat = "==22.10.0"
backports-zoneinfo = "==0.2.1"
bitarray = "==2.9.2"
blinker = "==1.8.2"
bytestring-splitter = "==2.4.1"
certifi = "==2024.2.2"
cffi = "==1.16.0"
charset-normalizer = "==3.3.2"
click = "==8.1.7"
colorama = "==0.4.6"
constant-sorrow = "==0.1.0a9"
constantly = "==23.10.4"
cryptography = "==42.0.7"
cytoolz = "==0.12.3"
dateparser = "==1.2.0"
eth-abi = "==4.2.1"
eth-account = "==0.10.0"
eth-hash = {extras = ["pycryptodome"], version = "==0.7.0"}
eth-keyfile = "==0.8.0"
eth-keys = "==0.4.0"
eth-rlp = "==1.0.1"
eth-typing = "==3.5.2"
eth-utils = "==2.3.1"
flask = "==3.0.3"
frozenlist = "==1.4.1"
hendrix = "==5.0.0"
hexbytes = "==0.3.1"
humanize = "==4.9.0"
hyperlink = "==21.0.0"
idna = "==3.7"
importlib-metadata = "==7.1.0"
importlib-resources = "==6.4.0"
incremental = "==22.10.0"
itsdangerous = "==2.2.0"
jinja2 = "==3.1.4"
jsonpath-ng = "==1.6.1"
jsonschema-specifications = "==2023.12.1"
jsonschema = "==4.21.1"
lru-dict = "==1.2.0"
mako = "==1.3.5"
markupsafe = "==2.1.5"
marshmallow = "==3.21.2"
maya = "==0.6.1"
mnemonic = "==0.20"
msgpack-python = "==0.5.6"
multidict = "==6.0.5"
nucypher-core = "==0.13.0"
packaging = "==23.2"
parsimonious = "==0.9.0"
pendulum = "==3.0.0"
pkgutil-resolve-name = "==1.3.10"
prometheus-client = "==0.20.0"
protobuf = "==5.26.1"
pyasn1-modules = "==0.4.0"
pyasn1 = "==0.6.0"
pychalk = "==2.0.1"
pycparser = "==2.22"
pycryptodome = "==3.20.0"
pydantic-core = "==2.18.2"
pydantic = "==2.7.1"
pynacl = "==1.5.0"
pyopenssl = "==24.1.0"
python-dateutil = "==2.8.2"
python-statemachine = "==2.1.2"
pytz = "==2024.1"
pyunormalize = "==15.1.0"
pywin32 = "==306"
referencing = "==0.34.0"
regex = "==2023.12.25"
requests = "==2.31.0"
rlp = "==3.0.0"
rpds-py = "==0.18.0"
service-identity = "==24.1.0"
siwe = "==2.4.1"
six = "==1.16.0"
snaptime = "==0.2.4"
tabulate = "==0.9.0"
time-machine = "==2.14.1"
toolz = "==0.12.1"
twisted-iocpsupport = "==1.0.4"
twisted = "==24.3.0"
txaio = "==23.1.1"
typing-extensions = "==4.11.0"
tzdata = "==2024.1"
tzlocal = "==5.2"
urllib3 = "==2.2.0"
watchdog = "==3.0.0"
web3 = "==6.15.1"
websockets = "==12.0"
werkzeug = "==3.0.3"
yarl = "==1.9.4"
zipp = "==3.18.1"
zope-interface = "==6.2"
[dev-packages]
[requires]
python_version = "3.12"

View File

@ -73,8 +73,8 @@ class ConditionType(Enum):
TIME = "time"
CONTRACT = "contract"
RPC = "rpc"
JSONAPI = "json-api"
COMPOUND = "compound"
OFFCHAIN = "offchain"
@classmethod
def values(cls) -> List[str]:
@ -417,7 +417,7 @@ class ConditionLingo(_Serializable):
conditions expression framework.
"""
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
from nucypher.policy.conditions.offchain import OffchainCondition
from nucypher.policy.conditions.offchain import JsonApiCondition
from nucypher.policy.conditions.time import TimeCondition
# version logical adjustments can be made here as required
@ -428,7 +428,7 @@ class ConditionLingo(_Serializable):
ContractCondition,
RPCCondition,
CompoundAccessControlCondition,
OffchainCondition,
JsonApiCondition,
):
if condition.CONDITION_TYPE == condition_type:
return condition

View File

@ -3,7 +3,7 @@ from typing import Any, Optional, Tuple
import requests
from jsonpath_ng.exceptions import JsonPathLexerError, JsonPathParserError
from jsonpath_ng.ext import parse
from marshmallow import ValidationError, fields, post_load, validate
from marshmallow import fields, post_load, validate
from marshmallow.fields import Field
from nucypher.policy.conditions.base import AccessControlCondition
@ -17,33 +17,36 @@ from nucypher.utilities.logging import Logger
class JSONPathField(Field):
default_error_messages = {"invalid": "Not a valid JSONPath expression."}
default_error_messages = {
"invalidType": "Expression of type '{type(value)}' is not valid for JSONPath",
"invalid": "'{value}' is not a valid JSONPath expression",
}
def _deserialize(self, value, attr, data, **kwargs):
if not isinstance(value, str):
self.fail("invalid")
raise self.make_error("invalidType", input=value)
try:
parse(value)
except (JsonPathLexerError, JsonPathParserError):
self.fail("invalid")
raise self.make_error("invalid", value=value)
return value
class OffchainCondition(AccessControlCondition):
class JsonApiCondition(AccessControlCondition):
"""
An offchain condition is a condition that can be evaluated by reading from a JSON
endpoint. This may be a REST service but the only requirement is that
the response is JSON and can be parsed using jsonpath.
A JSON API condition is a condition that can be evaluated by reading from a JSON
HTTPS endpoint. The response must return an HTTP 200 with valid JSON in the response body.
The response will be deserialized as JSON and parsed using jsonpath.
"""
CONDITION_TYPE = ConditionType.OFFCHAIN.value
LOGGER = Logger("nucypher.policy.conditions.offchain")
CONDITION_TYPE = ConditionType.JSONAPI.value
LOGGER = Logger("nucypher.policy.conditions.JsonApiCondition")
class Schema(CamelCaseSchema):
name = fields.Str(required=False)
condition_type = fields.Str(
validate=validate.Equal(ConditionType.OFFCHAIN.value), required=True
validate=validate.Equal(ConditionType.JSONAPI.value), required=True
)
headers = fields.Dict(required=False)
parameters = fields.Dict(required=False)
@ -53,15 +56,9 @@ class OffchainCondition(AccessControlCondition):
ReturnValueTest.ReturnValueTestSchema(), required=True
)
def validate_query(self, value):
try:
parse(value)
except Exception as e:
raise ValidationError(f"Invalid JSONPath query: {e}")
@post_load
def make(self, data, **kwargs):
return OffchainCondition(**data)
return JsonApiCondition(**data)
def __init__(
self,
@ -70,7 +67,7 @@ class OffchainCondition(AccessControlCondition):
return_value_test: ReturnValueTest,
headers: Optional[dict] = None,
parameters: Optional[dict] = None,
condition_type: str = ConditionType.OFFCHAIN.value,
condition_type: str = ConditionType.JSONAPI.value,
):
if condition_type != self.CONDITION_TYPE:
raise InvalidCondition(
@ -102,6 +99,14 @@ class OffchainCondition(AccessControlCondition):
f"Failed to fetch endpoint {self.endpoint}: {request_error}"
)
if response.status_code != 200:
self.logger.error(
f"Failed to fetch endpoint {self.endpoint}: {response.status_code}"
)
raise ConditionEvaluationFailed(
f"Failed to fetch endpoint {self.endpoint}: {response.status_code}"
)
return response
def deserialize_response(self, response: requests.Response) -> Any:

View File

@ -9,7 +9,7 @@ from nucypher.policy.conditions.exceptions import (
InvalidCondition,
)
from nucypher.policy.conditions.lingo import ConditionLingo, ReturnValueTest
from nucypher.policy.conditions.offchain import JSONPathField, OffchainCondition
from nucypher.policy.conditions.offchain import JsonApiCondition, JSONPathField
def test_jsonpath_field_valid():
@ -24,11 +24,13 @@ def test_jsonpath_field_invalid():
invalid_jsonpath = "invalid jsonpath"
with pytest.raises(ValidationError) as excinfo:
field.deserialize(invalid_jsonpath)
assert "Not a valid JSONPath expression." in str(excinfo.value)
assert f"'{invalid_jsonpath}' is not a valid JSONPath expression" in str(
excinfo.value
)
def test_offchain_condition_initialization():
condition = OffchainCondition(
def test_json_api_condition_initialization():
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0),
@ -38,23 +40,23 @@ def test_offchain_condition_initialization():
assert condition.return_value_test.eval(0)
def test_offchain_condition_invalid_type():
def test_json_api_condition_invalid_type():
with pytest.raises(InvalidCondition) as excinfo:
OffchainCondition(
JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0),
condition_type="INVALID_TYPE",
)
assert "must be instantiated with the offchain type" in str(excinfo.value)
assert "must be instantiated with the json-api type" in str(excinfo.value)
def test_offchain_condition_fetch(mocker):
def test_json_api_condition_fetch(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"title": "Test Title"}]}}
mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition(
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].title",
return_value_test=ReturnValueTest("==", "'Test Title'"),
@ -64,12 +66,12 @@ def test_offchain_condition_fetch(mocker):
assert response.json() == {"store": {"book": [{"title": "Test Title"}]}}
def test_offchain_condition_fetch_failure(mocker):
def test_json_api_condition_fetch_failure(mocker):
mocker.patch(
"requests.get", side_effect=requests.exceptions.RequestException("Error")
)
condition = OffchainCondition(
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "1"),
@ -79,12 +81,12 @@ def test_offchain_condition_fetch_failure(mocker):
assert "Failed to fetch endpoint" in str(excinfo.value)
def test_offchain_condition_verify(mocker):
def test_json_api_condition_verify(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"price": "1"}]}}
mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition(
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "1"),
@ -94,12 +96,12 @@ def test_offchain_condition_verify(mocker):
assert value == "1"
def test_offchain_condition_verify_invalid_json(mocker):
def test_json_api_condition_verify_invalid_json(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.side_effect = requests.exceptions.RequestException("Error")
mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition(
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "2"),
@ -118,7 +120,7 @@ def test_non_json_response(mocker):
mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition(
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "18"),
@ -130,9 +132,7 @@ def test_non_json_response(mocker):
assert "Failed to parse JSON response" in str(excinfo.value)
def test_basic_offchain_condition_evaluation_with_parameters(
accounts, condition_providers, mocker
):
def test_basic_json_api_condition_evaluation_with_parameters(mocker):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(
@ -140,7 +140,7 @@ def test_basic_offchain_condition_evaluation_with_parameters(
),
)
condition = OffchainCondition(
condition = JsonApiCondition(
endpoint="https://api.coingecko.com/api/v3/simple/price",
parameters={
"ids": "ethereum",
@ -154,9 +154,7 @@ def test_basic_offchain_condition_evaluation_with_parameters(
assert mocked_get.call_count == 1
def test_basic_offchain_condition_evaluation_with_headers(
accounts, condition_providers, mocker
):
def test_basic_json_api_condition_evaluation_with_headers(mocker):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(
@ -164,7 +162,7 @@ def test_basic_offchain_condition_evaluation_with_headers(
),
)
condition = OffchainCondition(
condition = JsonApiCondition(
endpoint="https://api.coingecko.com/api/v3/simple/price",
parameters={
"ids": "ethereum",
@ -180,9 +178,9 @@ def test_basic_offchain_condition_evaluation_with_headers(
assert mocked_get.call_args[1]["headers"]["Authorization"] == "Bearer 1234567890"
def test_offchain_condition_from_lingo_expression():
def test_json_api_condition_from_lingo_expression():
lingo_dict = {
"conditionType": "offchain",
"conditionType": "json-api",
"endpoint": "https://api.example.com/data",
"query": "$.store.book[0].price",
"parameters": {
@ -199,8 +197,8 @@ def test_offchain_condition_from_lingo_expression():
}
cls = ConditionLingo.resolve_condition_class(lingo_dict, version=1.0)
assert cls == OffchainCondition
assert cls == JsonApiCondition
lingo_json = json.dumps(lingo_dict)
condition = OffchainCondition.from_json(lingo_json)
assert isinstance(condition, OffchainCondition)
condition = JsonApiCondition.from_json(lingo_json)
assert isinstance(condition, JsonApiCondition)