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

View File

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

View File

@ -9,7 +9,7 @@ from nucypher.policy.conditions.exceptions import (
InvalidCondition, InvalidCondition,
) )
from nucypher.policy.conditions.lingo import ConditionLingo, ReturnValueTest 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(): def test_jsonpath_field_valid():
@ -24,11 +24,13 @@ def test_jsonpath_field_invalid():
invalid_jsonpath = "invalid jsonpath" invalid_jsonpath = "invalid jsonpath"
with pytest.raises(ValidationError) as excinfo: with pytest.raises(ValidationError) as excinfo:
field.deserialize(invalid_jsonpath) 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(): def test_json_api_condition_initialization():
condition = OffchainCondition( condition = JsonApiCondition(
endpoint="https://api.example.com/data", endpoint="https://api.example.com/data",
query="$.store.book[0].price", query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0), return_value_test=ReturnValueTest("==", 0),
@ -38,23 +40,23 @@ def test_offchain_condition_initialization():
assert condition.return_value_test.eval(0) 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: with pytest.raises(InvalidCondition) as excinfo:
OffchainCondition( JsonApiCondition(
endpoint="https://api.example.com/data", endpoint="https://api.example.com/data",
query="$.store.book[0].price", query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0), return_value_test=ReturnValueTest("==", 0),
condition_type="INVALID_TYPE", 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 = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"title": "Test Title"}]}} mock_response.json.return_value = {"store": {"book": [{"title": "Test Title"}]}}
mocker.patch("requests.get", return_value=mock_response) mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition( condition = JsonApiCondition(
endpoint="https://api.example.com/data", endpoint="https://api.example.com/data",
query="$.store.book[0].title", query="$.store.book[0].title",
return_value_test=ReturnValueTest("==", "'Test 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"}]}} 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( mocker.patch(
"requests.get", side_effect=requests.exceptions.RequestException("Error") "requests.get", side_effect=requests.exceptions.RequestException("Error")
) )
condition = OffchainCondition( condition = JsonApiCondition(
endpoint="https://api.example.com/data", endpoint="https://api.example.com/data",
query="$.store.book[0].price", query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "1"), 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) 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 = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"price": "1"}]}} mock_response.json.return_value = {"store": {"book": [{"price": "1"}]}}
mocker.patch("requests.get", return_value=mock_response) mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition( condition = JsonApiCondition(
endpoint="https://api.example.com/data", endpoint="https://api.example.com/data",
query="$.store.book[0].price", query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "1"), return_value_test=ReturnValueTest("==", "1"),
@ -94,12 +96,12 @@ def test_offchain_condition_verify(mocker):
assert value == "1" 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 = mocker.Mock(status_code=200)
mock_response.json.side_effect = requests.exceptions.RequestException("Error") mock_response.json.side_effect = requests.exceptions.RequestException("Error")
mocker.patch("requests.get", return_value=mock_response) mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition( condition = JsonApiCondition(
endpoint="https://api.example.com/data", endpoint="https://api.example.com/data",
query="$.store.book[0].price", query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "2"), return_value_test=ReturnValueTest("==", "2"),
@ -118,7 +120,7 @@ def test_non_json_response(mocker):
mocker.patch("requests.get", return_value=mock_response) mocker.patch("requests.get", return_value=mock_response)
condition = OffchainCondition( condition = JsonApiCondition(
endpoint="https://api.example.com/data", endpoint="https://api.example.com/data",
query="$.store.book[0].price", query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", "18"), 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) assert "Failed to parse JSON response" in str(excinfo.value)
def test_basic_offchain_condition_evaluation_with_parameters( def test_basic_json_api_condition_evaluation_with_parameters(mocker):
accounts, condition_providers, mocker
):
mocked_get = mocker.patch( mocked_get = mocker.patch(
"requests.get", "requests.get",
return_value=mocker.Mock( 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", endpoint="https://api.coingecko.com/api/v3/simple/price",
parameters={ parameters={
"ids": "ethereum", "ids": "ethereum",
@ -154,9 +154,7 @@ def test_basic_offchain_condition_evaluation_with_parameters(
assert mocked_get.call_count == 1 assert mocked_get.call_count == 1
def test_basic_offchain_condition_evaluation_with_headers( def test_basic_json_api_condition_evaluation_with_headers(mocker):
accounts, condition_providers, mocker
):
mocked_get = mocker.patch( mocked_get = mocker.patch(
"requests.get", "requests.get",
return_value=mocker.Mock( 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", endpoint="https://api.coingecko.com/api/v3/simple/price",
parameters={ parameters={
"ids": "ethereum", "ids": "ethereum",
@ -180,9 +178,9 @@ def test_basic_offchain_condition_evaluation_with_headers(
assert mocked_get.call_args[1]["headers"]["Authorization"] == "Bearer 1234567890" 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 = { lingo_dict = {
"conditionType": "offchain", "conditionType": "json-api",
"endpoint": "https://api.example.com/data", "endpoint": "https://api.example.com/data",
"query": "$.store.book[0].price", "query": "$.store.book[0].price",
"parameters": { "parameters": {
@ -199,8 +197,8 @@ def test_offchain_condition_from_lingo_expression():
} }
cls = ConditionLingo.resolve_condition_class(lingo_dict, version=1.0) cls = ConditionLingo.resolve_condition_class(lingo_dict, version=1.0)
assert cls == OffchainCondition assert cls == JsonApiCondition
lingo_json = json.dumps(lingo_dict) lingo_json = json.dumps(lingo_dict)
condition = OffchainCondition.from_json(lingo_json) condition = JsonApiCondition.from_json(lingo_json)
assert isinstance(condition, OffchainCondition) assert isinstance(condition, JsonApiCondition)