mirror of https://github.com/nucypher/nucypher.git
respond to RFCs in PR #3511
parent
f59d636d54
commit
f2c7337483
113
Pipfile
113
Pipfile
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue