Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improve Request.from_dict to have fewer exceptions to the rule #726

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5a3ba97
improve Request.from_dict
mvadari Jul 11, 2024
b16e842
update changelog
mvadari Jul 11, 2024
c160a6e
better erroring for amm_info
mvadari Jul 11, 2024
de91678
improve generalization
mvadari Jul 11, 2024
0673861
complete codecov
mvadari Jul 11, 2024
0eb6241
add TODO
mvadari Jul 11, 2024
3160497
remove commented out code
mvadari Jul 11, 2024
2228ee1
clean up
mvadari Jul 11, 2024
14e76f6
improve comment
mvadari Jul 11, 2024
a3d68d4
fix(?) flakiness in snippet
mvadari Jul 12, 2024
c8dc831
improve comment
mvadari Jul 16, 2024
3982553
fix nft_history
mvadari Jul 16, 2024
45f1366
Merge branch 'main' into request-from-dict
mvadari Jul 17, 2024
e309bbb
fix error message
mvadari Jul 18, 2024
1946b65
alphabetize
mvadari Aug 5, 2024
1d67dd5
Merge branch 'main' into request-from-dict
mvadari Aug 27, 2024
a8d3c9d
Merge branch 'main' into request-from-dict
mvadari Sep 26, 2024
6d4fdbc
Merge branch 'main' into request-from-dict
mvadari Oct 8, 2024
aad0a68
Merge branch 'main' into request-from-dict
mvadari Oct 25, 2024
2b2319a
Merge branch 'main' into request-from-dict
mvadari Nov 1, 2024
8d155b8
Merge branch 'main' into request-from-dict
mvadari Nov 7, 2024
6b1074d
Merge branch 'main' into request-from-dict
mvadari Nov 11, 2024
9d994d7
Merge branch 'main' into request-from-dict
mvadari Dec 12, 2024
3fd5a38
Merge branch 'main' into request-from-dict
mvadari Dec 13, 2024
6b2dd0e
Merge branch 'main' into request-from-dict
mvadari Dec 20, 2024
6adc111
Merge branch 'main' into request-from-dict
mvadari Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- Allow empty strings for the purpose of removing fields in DIDSet transaction
- Added support for `amm_info` to `Request.from_dict`
- Improved erroring for `amm_info`
mvadari marked this conversation as resolved.
Show resolved Hide resolved

### Removed
- Remove deprecated `full`, `accounts`, and `type` parameters from ledger request model
Expand Down
1 change: 1 addition & 0 deletions snippets/bridge_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
f"Destination account {wallet2.classic_address} has been created via the "
"bridge"
)
sleep(0.5) # to avoid flakiness
initial_balance = get_balance(wallet2.classic_address, issuing_client)
break

Expand Down
28 changes: 28 additions & 0 deletions tests/unit/models/requests/test_amm_info.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from unittest import TestCase

from xrpl.models.currencies import XRP, IssuedCurrency
from xrpl.models.exceptions import XRPLModelException
from xrpl.models.requests import AMMInfo

_ASSET = XRP()
_ASSET_2 = IssuedCurrency(currency="USD", issuer="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj")
_ACCOUNT = _ASSET_2.issuer
mvadari marked this conversation as resolved.
Show resolved Hide resolved


class TestAMMInfo(TestCase):
Expand All @@ -14,3 +16,29 @@ def test_asset_asset2(self):
asset2=_ASSET_2,
)
self.assertTrue(request.is_valid())

def test_amount(self):
request = AMMInfo(
amm_account=_ACCOUNT,
)
self.assertTrue(request.is_valid())

def test_all_three(self):
with self.assertRaises(XRPLModelException):
AMMInfo(
amm_account=_ACCOUNT,
asset=_ASSET,
asset2=_ASSET_2,
)

def test_only_asset(self):
with self.assertRaises(XRPLModelException):
AMMInfo(
asset=_ASSET,
)

def test_only_one_asset2(self):
with self.assertRaises(XRPLModelException):
AMMInfo(
asset2=_ASSET_2,
)
66 changes: 61 additions & 5 deletions tests/unit/models/requests/test_requests.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,71 @@
from unittest import TestCase

from xrpl.models.requests import Fee, GenericRequest
from xrpl.models.exceptions import XRPLModelException
from xrpl.models.requests import Fee, GenericRequest, Request


class TestRequest(TestCase):
def test_to_dict_includes_method_as_string(self):
tx = Fee()
value = tx.to_dict()["method"]
req = Fee()
value = req.to_dict()["method"]
self.assertEqual(type(value), str)

def test_generic_request_to_dict_sets_command_as_method(self):
command = "validator_list_sites"
tx = GenericRequest(command=command).to_dict()
self.assertDictEqual(tx, {"method": command})
req = GenericRequest(command=command).to_dict()
self.assertDictEqual(req, {"method": command})

def test_from_dict(self):
req = {"method": "account_tx", "account": "rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj"}
obj = Request.from_dict(req)
self.assertEqual(obj.__class__.__name__, "AccountTx")
expected = {**req, "binary": False, "forward": False}
self.assertDictEqual(obj.to_dict(), expected)

def test_from_dict_no_method(self):
req = {"account": "rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj"}
with self.assertRaises(XRPLModelException):
Request.from_dict(req)

def test_from_dict_wrong_method(self):
req = {"method": "account_tx"}
with self.assertRaises(XRPLModelException):
Fee.from_dict(req)

def test_from_dict_noripple_check(self):
req = {
"method": "noripple_check",
"account": "rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj",
"role": "user",
}
obj = Request.from_dict(req)
self.assertEqual(obj.__class__.__name__, "NoRippleCheck")
expected = {**req, "transactions": False, "limit": 300}
self.assertDictEqual(obj.to_dict(), expected)

def test_from_dict_account_nfts(self):
req = {
"method": "account_nfts",
"account": "rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj",
}
obj = Request.from_dict(req)
self.assertEqual(obj.__class__.__name__, "AccountNFTs")
self.assertDictEqual(obj.to_dict(), req)

def test_from_dict_amm_info(self):
req = {
"method": "amm_info",
"amm_account": "rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj",
}
obj = Request.from_dict(req)
self.assertEqual(obj.__class__.__name__, "AMMInfo")
self.assertDictEqual(obj.to_dict(), req)

def test_from_dict_generic_request(self):
req = {
"method": "tx_history",
"start": 0,
}
obj = Request.from_dict(req)
self.assertEqual(obj.__class__.__name__, "GenericRequest")
self.assertDictEqual(obj.to_dict(), req)
mvadari marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 3 additions & 1 deletion xrpl/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
f"(?:{_CAMEL_CASE_LEADING_LOWER}|{_CAMEL_CASE_ABBREVIATION}|{_CAMEL_CASE_TYPICAL})"
)
# This is used to make exceptions when converting dictionary keys to xrpl JSON
# keys. We snake case keys, but some keys are abbreviations.
# keys. xrpl-py uses snake case keys, but some keys are abbreviations.
ABBREVIATIONS: Final[Dict[str, str]] = {
"amm": "AMM",
"did": "DID",
Expand All @@ -44,6 +44,8 @@
"unl": "UNL",
"uri": "URI",
"xchain": "XChain",
"nfts": "NFTs",
"noripple": "NoRipple",
}


Expand Down
13 changes: 12 additions & 1 deletion xrpl/models/requests/amm_info.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""This request gets information about an Automated Market Maker (AMM) instance."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional
from typing import Dict, Optional

from typing_extensions import Self

from xrpl.models.currencies import Currency
from xrpl.models.requests.request import Request, RequestMethod
Expand Down Expand Up @@ -33,3 +36,11 @@ class AMMInfo(Request):
"""

method: RequestMethod = field(default=RequestMethod.AMM_INFO, init=False)

def _get_errors(self: Self) -> Dict[str, str]:
errors = super()._get_errors()
if (self.asset is None) != (self.asset2 is None):
errors["assets"] = "Must have both `asset` and `asset2` fields."
if (self.asset is None) == (self.amm_account is None):
errors["params"] = "Must not have both asset and `amm_account` fields."
return errors
26 changes: 9 additions & 17 deletions xrpl/models/requests/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing_extensions import Self

import xrpl.models.requests # bare import to get around circular dependency
from xrpl.models.base_model import BaseModel
from xrpl.models.base_model import ABBREVIATIONS, BaseModel
from xrpl.models.exceptions import XRPLModelException
from xrpl.models.required import REQUIRED
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init
Expand Down Expand Up @@ -122,6 +122,8 @@ def from_dict(cls: Type[Self], value: Dict[str, Any]) -> Self:
Raises:
XRPLModelException: If the dictionary provided is invalid.
"""
# TODO: add support for "command" parameter and proper JSON RPC format
# This is already done in `GenericRequest`, for reference
if cls.__name__ == "Request":
if "method" not in value:
raise XRPLModelException("Request does not include method.")
Expand Down Expand Up @@ -159,22 +161,12 @@ def get_method(cls: Type[Self], method: str) -> Type[Request]:
The request class with the given name. If the request doesn't exist, then
it will return a `GenericRequest`.
"""
# special case for NoRippleCheck and NFT methods
if method == RequestMethod.NO_RIPPLE_CHECK:
return xrpl.models.requests.NoRippleCheck
if method == RequestMethod.ACCOUNT_NFTS:
return xrpl.models.requests.AccountNFTs
if method == RequestMethod.NFT_BUY_OFFERS:
return xrpl.models.requests.NFTBuyOffers
if method == RequestMethod.NFT_SELL_OFFERS:
return xrpl.models.requests.NFTSellOffers
if method == RequestMethod.NFT_INFO:
return xrpl.models.requests.NFTInfo
if method == RequestMethod.NFT_HISTORY:
return xrpl.models.requests.NFTHistory
if method == RequestMethod.NFTS_BY_ISSUER:
return xrpl.models.requests.NFTsByIssuer
parsed_name = "".join([word.capitalize() for word in method.split("_")])
parsed_name = "".join(
mvadari marked this conversation as resolved.
Show resolved Hide resolved
[
ABBREVIATIONS[word] if word in ABBREVIATIONS else word.capitalize()
for word in method.split("_")
]
)
if parsed_name in xrpl.models.requests.__all__:
return cast(Type[Request], getattr(xrpl.models.requests, parsed_name))
return xrpl.models.requests.GenericRequest
Expand Down
Loading