From e0cd844da939ae71681b8a7a65eeed9224449163 Mon Sep 17 00:00:00 2001 From: "Chris (Someguy123)" Date: Sun, 21 Jul 2019 16:50:07 +0100 Subject: [PATCH] v1.1.0 - Major update - return instances instead of dicts **Data classes / objects** As many of us know, it gets tiring having to constantly type `x['balance']` when something returns a dictionary, especially when there's no IDE completion for it. To solve this problem, new classes have been written to represent SteemEngine objects, such as balances, transactions, and tokens. This allows you to access keys simply through `x.balance` instead. For backwards compatibility, the `ObjBase` parent class allows instances of these data classes to be treated like dictionaries/lists, so `x['balance']` will still work, and it will return the same format as before, while `x.balance` for example, would return the balance as a proper `Decimal` object. You can also convert the classes into dicts/lists with `dict(x)` and `list(x)`, but be aware that this uses the `raw_data` that was originally passed, so these converted objects will not have their keys casted to the appropriate Python types, unless they were casted before being passed into the class. - New module `privex.steemengine.objects` for classes designed to hold data - `ObjBase` is used as the base class for the data classes. For backwards compatibility, it allows instances of the classes to be accessed like a dictionary/list, offers `from_list` for easy conversions of `list` into their respective data classes, and also implements `__iter__` so instances can be converted using `dict(x)` and `list(x)`. - `Token` represents the data of a SteemEngine token such as name/symbol/supply - `SEBalance` represents a balance with account/symbol/balance - `SETransaction` represents a history tx with txid/timestamp/quantity etc. - `TokenMetadata` represents the `metadata` field of token data **Upgraded SteemEngineToken to use the data classes** The **SteemEngineToken** class has been updated to use the new data classes. Thanks to `ObjBase`, this should hopefully not break any existing applications, but there's always a risk. The methods that have been updated: - `list_tokens` now returns `List[Token]` instead of `List[dict]` - `get_token` now returns `Token` instead of `dict` - `get_token_balances` now returns `List[SEBalance]` instead of `List[dict]` - `list_transactions` now returns `List[SETransaction]` instead of `List[dict]` **Documentation for the new data classes** The documentation has been updated with details on how the data classes work. The documentation for **SteemEngineToken** methods have *mostly* been updated to show usage of the new data classes instead of `dict`s. --- docs/source/code/index.rst | 1 + .../code/privex.steemengine.objects.rst | 27 ++++ .../privex.steemengine.objects.ObjBase.rst | 23 +++ .../privex.steemengine.objects.SEBalance.rst | 23 +++ ...ivex.steemengine.objects.SETransaction.rst | 23 +++ .../privex.steemengine.objects.Token.rst | 23 +++ ...ivex.steemengine.objects.TokenMetadata.rst | 23 +++ privex/steemengine/SteemEngineToken.py | 47 +++--- privex/steemengine/__init__.py | 1 + privex/steemengine/objects.py | 153 ++++++++++++++++++ setup.py | 2 +- tests.py | 2 +- 12 files changed, 326 insertions(+), 22 deletions(-) create mode 100644 docs/source/code/privex.steemengine.objects.rst create mode 100644 docs/source/code/stubs/privex.steemengine.objects.ObjBase.rst create mode 100644 docs/source/code/stubs/privex.steemengine.objects.SEBalance.rst create mode 100644 docs/source/code/stubs/privex.steemengine.objects.SETransaction.rst create mode 100644 docs/source/code/stubs/privex.steemengine.objects.Token.rst create mode 100644 docs/source/code/stubs/privex.steemengine.objects.TokenMetadata.rst create mode 100644 privex/steemengine/objects.py diff --git a/docs/source/code/index.rst b/docs/source/code/index.rst index 6542e0a..a1e8295 100644 --- a/docs/source/code/index.rst +++ b/docs/source/code/index.rst @@ -4,4 +4,5 @@ privex.steemengine.SteemEngineToken.SteemEngineToken privex.steemengine.SteemEngineHistory.SteemEngineHistory privex.steemengine.exceptions + privex.steemengine.objects tests \ No newline at end of file diff --git a/docs/source/code/privex.steemengine.objects.rst b/docs/source/code/privex.steemengine.objects.rst new file mode 100644 index 0000000..9cb46f0 --- /dev/null +++ b/docs/source/code/privex.steemengine.objects.rst @@ -0,0 +1,27 @@ +Data Objects (Token/SEBalance etc.) +==================================== + +.. automodule:: privex.steemengine.objects + + + + + + + + .. rubric:: Classes + + .. autosummary:: + :toctree: stubs + + ObjBase + SEBalance + SETransaction + Token + TokenMetadata + + + + + + \ No newline at end of file diff --git a/docs/source/code/stubs/privex.steemengine.objects.ObjBase.rst b/docs/source/code/stubs/privex.steemengine.objects.ObjBase.rst new file mode 100644 index 0000000..2e4b5c5 --- /dev/null +++ b/docs/source/code/stubs/privex.steemengine.objects.ObjBase.rst @@ -0,0 +1,23 @@ +objects.ObjBase +================================== + +.. currentmodule:: privex.steemengine.objects + +.. autoclass:: ObjBase + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~ObjBase.__init__ + ~ObjBase.from_list + + + + + + \ No newline at end of file diff --git a/docs/source/code/stubs/privex.steemengine.objects.SEBalance.rst b/docs/source/code/stubs/privex.steemengine.objects.SEBalance.rst new file mode 100644 index 0000000..3b2cded --- /dev/null +++ b/docs/source/code/stubs/privex.steemengine.objects.SEBalance.rst @@ -0,0 +1,23 @@ +objects.SEBalance +==================================== + +.. currentmodule:: privex.steemengine.objects + +.. autoclass:: SEBalance + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~SEBalance.__init__ + ~SEBalance.from_list + + + + + + \ No newline at end of file diff --git a/docs/source/code/stubs/privex.steemengine.objects.SETransaction.rst b/docs/source/code/stubs/privex.steemengine.objects.SETransaction.rst new file mode 100644 index 0000000..69dcb54 --- /dev/null +++ b/docs/source/code/stubs/privex.steemengine.objects.SETransaction.rst @@ -0,0 +1,23 @@ +objects.SETransaction +======================================== + +.. currentmodule:: privex.steemengine.objects + +.. autoclass:: SETransaction + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~SETransaction.__init__ + ~SETransaction.from_list + + + + + + \ No newline at end of file diff --git a/docs/source/code/stubs/privex.steemengine.objects.Token.rst b/docs/source/code/stubs/privex.steemengine.objects.Token.rst new file mode 100644 index 0000000..261ad43 --- /dev/null +++ b/docs/source/code/stubs/privex.steemengine.objects.Token.rst @@ -0,0 +1,23 @@ +objects.Token +================================ + +.. currentmodule:: privex.steemengine.objects + +.. autoclass:: Token + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~Token.__init__ + ~Token.from_list + + + + + + \ No newline at end of file diff --git a/docs/source/code/stubs/privex.steemengine.objects.TokenMetadata.rst b/docs/source/code/stubs/privex.steemengine.objects.TokenMetadata.rst new file mode 100644 index 0000000..444d57a --- /dev/null +++ b/docs/source/code/stubs/privex.steemengine.objects.TokenMetadata.rst @@ -0,0 +1,23 @@ +objects.TokenMetadata +======================================== + +.. currentmodule:: privex.steemengine.objects + +.. autoclass:: TokenMetadata + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~TokenMetadata.__init__ + ~TokenMetadata.from_list + + + + + + \ No newline at end of file diff --git a/privex/steemengine/SteemEngineToken.py b/privex/steemengine/SteemEngineToken.py index 32f9306..bbf8775 100644 --- a/privex/steemengine/SteemEngineToken.py +++ b/privex/steemengine/SteemEngineToken.py @@ -4,6 +4,7 @@ from privex.jsonrpc import SteemEngineRPC from privex.steemengine import exceptions from privex.steemengine.SteemEngineHistory import SteemEngineHistory +from privex.steemengine.objects import SEBalance, SETransaction, Token from privex.helpers import empty log = logging.getLogger(__name__) @@ -96,7 +97,7 @@ def custom_beem(node: Union[str, list] = "", *args, **kwargs): SteemEngineToken._steem = Steem(node, *args, **kwargs) return SteemEngineToken._steem - def get_balances(self, user) -> List[dict]: + def get_balances(self, user) -> List[SEBalance]: """ Get all token balances for a user. @@ -104,20 +105,20 @@ def get_balances(self, user) -> List[dict]: >>> balances = SteemEngineToken().get_balances('someguy123') >>> for bal in balances: - ... print(f"{bal['symbol']} balance is: {bal['balance']}") + ... print(f"{bal.symbol} balance is: {bal.balance}") ENG balance is: 12.345 SGTK balance is: 51235 STEEMP balance is: 102.437 :param user: Username to find all token balances for - :return list balances: All balances of a user [{account:str, symbol:str, balance:str}...] + :return list balances: All balances of a user [{account:str, symbol:str, balance:str}...] """ log.debug('Finding all token balances for user %s', user) - return self.rpc.find( + return list(SEBalance.from_list(self.rpc.find( contract='tokens', table='balances', query=dict(account=user) - ) + ))) def get_token_balance(self, user, symbol) -> Decimal: """ @@ -159,14 +160,14 @@ def account_exists(self, user) -> bool: log.debug('Checking if user %s exists', user) return len(self.steem.rpc.get_account(user)) > 0 - def list_tokens(self, limit=1000, offset=0) -> List[dict]: + def list_tokens(self, limit=1000, offset=0) -> List[Token]: """ - Returns a list of all tokens. + Returns a list of all tokens as :class:`.Token` objects. **Example:** >>> for t in SteemEngineToken().list_tokens(): - ... print(f"Token {t['symbol']} has a max supply of {t['maxSupply']} and issued by {t['issuer']}") + ... print(f"Token {t.symbol} has a max supply of {t.max_supply} and issued by {t.issuer}") Token ENG has a max supply of 9007199254740991 and issued by null Token STEEMP has a max supply of 1000000000000 and issued by steem-peg Token BTCP has a max supply of 1000000000000 and issued by btcpeg @@ -174,7 +175,7 @@ def list_tokens(self, limit=1000, offset=0) -> List[dict]: :param limit: Amount of token objects to retrieve :param offset: Amount of token objects to skip (for pagination) - :return list tokens: Each list item formatted like this: + :return List tokens: Each :class:`.Token` list item formatted like this: .. code-block:: js @@ -190,12 +191,12 @@ def list_tokens(self, limit=1000, offset=0) -> List[dict]: } """ - return self.rpc.find( + return list(Token.from_list(self.rpc.find( contract='tokens', table='tokens', query={}, limit=limit, offset=offset - ) + ))) def find_steem_tx(self, tx_data: dict, last_blocks=15) -> dict: """ @@ -228,14 +229,14 @@ def find_steem_tx(self, tx_data: dict, last_blocks=15) -> dict: return tx return None - def list_transactions(self, user, symbol=None, limit=100, offset=0) -> List[dict]: + def list_transactions(self, user, symbol=None, limit=100, offset=0) -> List[SETransaction]: """ Get the Steem Engine transaction history for a given account **Example:** >>> for tx in SteemEngineToken().list_transactions('someguy123'): - ... print(tx['timestamp'], tx['from'], 'sent', tx['quantity'], tx['symbol'], 'to', tx['to']) + ... print(tx.timestamp, tx.sender, 'sent', tx.quantity, tx.symbol, 'to', tx.to) 2019-07-04T06:18:09.000Z market sent 100 SGTK to someguy123 2019-07-04T01:01:15.000Z minnowsupport sent 0.924 PAL to someguy123 2019-07-03T17:10:36.000Z someguy123 sent 1 BTSP to btsp @@ -244,27 +245,32 @@ def list_transactions(self, user, symbol=None, limit=100, offset=0) -> List[dict :param str symbol: Symbol to filter by, e.g. ENG (optional) :param int limit: Return this many transactions (optional) :param int offset: Skip this many transactions (for pagination) (optional) - :return List txs: A list of ``dict(block, txid, timestamp, symbol, from, from_type, to, to_type, memo, quantity)`` + :return List txs: A list of :class:`.SETransaction` containing + + block, txid, timestamp, symbol, sender, from_type, to, to_type, memo, quantity + """ symbol = None if empty(symbol) else symbol.upper() log.debug('Getting TX history for user %s, symbol %s, limit %s, offset %s', user, symbol, limit, offset) - return self.history_rpc.get_history(account=user, symbol=symbol, limit=limit, offset=offset) + return list(SETransaction.from_list(self.history_rpc.get_history(account=user, symbol=symbol, limit=limit, offset=offset))) - def get_token(self, symbol) -> dict: + def get_token(self, symbol) -> Token: """ Get the token object for an individual token. **Example:** >>> token = SteemEngineToken().get_token('SGTK'): - >>> print(token['issuer'], token['name']) + >>> print(token.issuer, token.name) someguy123 SomeToken :param str symbol: Symbol of the token to lookup, such as 'ENG' - :return dict token_data: A dictionary containing data about the token (see below) + :return Token token_data: An object containing data about the token (see below) - Formatted like below: + A :class:`.Token` object can be accessed either via attributes ``token.issuer`` or as a dict. + + They contain the fields below: .. code-block:: js @@ -282,11 +288,12 @@ def get_token(self, symbol) -> dict: :return None: If token not found, ``None`` is returned. """ log.debug('Getting token object for symbol %s', symbol) - return self.rpc.findone( + tk = self.rpc.findone( contract='tokens', table='tokens', query=dict(symbol=symbol.upper()) ) + return None if empty(tk) else Token(**tk) def send_token(self, symbol, from_acc, to_acc, amount: Decimal, memo="", find_tx=True) -> dict: """ diff --git a/privex/steemengine/__init__.py b/privex/steemengine/__init__.py index 03994d1..65d98d7 100644 --- a/privex/steemengine/__init__.py +++ b/privex/steemengine/__init__.py @@ -2,6 +2,7 @@ import sys from privex.steemengine.SteemEngineToken import SteemEngineToken from privex.steemengine.SteemEngineHistory import SteemEngineHistory +from privex.steemengine.objects import Token, TokenMetadata, SEBalance, SETransaction, ObjBase name = 'steemengine' diff --git a/privex/steemengine/objects.py b/privex/steemengine/objects.py new file mode 100644 index 0000000..61f764b --- /dev/null +++ b/privex/steemengine/objects.py @@ -0,0 +1,153 @@ +import json +from typing import Union, List, Generator +from decimal import Decimal +from privex.helpers import empty + +class ObjBase: + """ + A base class to be extended by data storage classes, allowing their attributes to be + accessed as if the class was a dict/list. + + Also allows the class to be converted into a dict/list if raw_data is filled, like so: ``dict(SomeClass())`` + """ + + def __init__(self, raw_data: Union[list, tuple, dict] = None, *args, **kwargs): + self.raw_data = {} if not raw_data else raw_data # type: Union[list, tuple, dict] + super(ObjBase, self).__init__(raw_data, *args, **kwargs) + + def __iter__(self): + r = self.raw_data + if type(r) is dict: + for k, v in r.items(): yield (k, v,) + return + for k, v in enumerate(r): yield (k, v,) + + def __getitem__(self, key): + """ + When the instance is accessed like a dict, try returning the matching attribute. + If the attribute doesn't exist, or the key is an integer, try and pull it from raw_data + """ + if type(key) is int: return self.raw_data[key] + if hasattr(self, key): return getattr(self, key) + if key in self.raw_data: return self.raw_data[key] + raise KeyError(key) + + @classmethod + def from_list(cls, obj_list: List[dict]): + """ + Converts a ``list`` of ``dict`` 's into a ``Generator[cls]`` of instances of the class you're calling this from. + + **Example:** + + >>> _balances = [dict(account='someguy123', symbol='SGTK', balance='1.234')] + >>> balances = list(SEBalance.from_list(_balances)) + >>> type(balances[0]) + + >>> balances[0].account + 'someguy123' + + """ + for tx in obj_list: + yield cls(**tx) + +class TokenMetadata(ObjBase): + """ + Represents the ``metadata`` field on a token object on SteemEngine + + :ivar str url: The official website for the token + :ivar str icon: A full URL to the icon for the token + :ivar str desc: A long description explaining the token + """ + def __init__(self, url="", icon="", desc="", **kwargs): + self.url, self.icon, self.desc = url, icon, desc + self.raw_data = {**kwargs, **dict(url=url,icon=icon,desc=desc)} + + +class Token(ObjBase): + """ + Represents a token's information on SteemEngine + + :ivar str symbol: The short symbol for the token, e.g. ``ENG`` + :ivar str name: The full name for the token, e.g. ``Steem Engine Token`` + :ivar str issuer: The username of the issuer/owner of the token on SteemEngine, e.g. ``someguy123`` + :ivar TokenMetadata metadata: Metadata for the token, including the ``url``, ``icon`` and ``desc`` (description) + :ivar int precision: The precision / amount of decimal places the token uses + :ivar Decimal max_supply: The maximum amount of tokens that can ever be printed + :ivar Decimal circulating_supply: Amount of tokens that are circulating, i.e. have not been burned + :ivar Decimal supply: Amount of tokens in existance + + """ + + def __init__(self, symbol, name="", issuer="", metadata: Union[str, dict] = None, **kwargs): + self.raw_data = {**kwargs, **dict(symbol=symbol,issuer=issuer,name=name, metadata=metadata)} + self.issuer, self.name, self.symbol = issuer, name, symbol # type: str + meta = metadata + meta = {} if empty(meta, itr=True) else (json.loads(meta) if type(meta) is str else meta) + self.metadata = TokenMetadata(**meta) # type: TokenMetadata + self.precision = int(kwargs.get('precision', 0)) # type: int + + _circ = _maxs = Decimal(0) + if 'maxSupply' in kwargs: _maxs = Decimal(kwargs['maxSupply']) + if 'max_supply' in kwargs: _maxs = Decimal(kwargs['max_supply']) + self.max_supply = self.maxSupply = _maxs # type: Decimal + + if 'circulatingSupply' in kwargs: _circ = Decimal(kwargs['circulatingSupply']) + if 'circulating_supply' in kwargs: _circ = Decimal(kwargs['circulating_supply']) + self.circulating_supply = self.circulatingSupply = _circ # type: Decimal + + self.supply = Decimal(kwargs.get('supply', '0')) # type: Decimal + + def __str__(self): + return f"" + + +class SETransaction(ObjBase): + """ + Represents a standard transaction from account history on SteemEngine + + :ivar int block: The block number of the transaction + :ivar str timestamp: The time the transaction occured, as a UTC-formatted string ``2019-07-04T06:18:09.000Z`` + :ivar str txid: The unique transaction ID of the transaction, as a string + :ivar str symbol: The short symbol for the token being sent/received, e.g. ``ENG`` + :ivar str sender: The Steem username that sent/issued the tokens + :ivar str to: The Steem username that received the tokens + :ivar str to_type: Either ``user`` (normal send/receive TX) or ``contract`` (issues/stakes etc.) + :ivar str from_type: Either ``user`` (normal send/receive TX) or ``contract`` (issues/stakes etc.) + :ivar str memo: A short message describing the purpose of the transaction + :ivar Decimal quantity: The amount of tokens that were sent/issued etc. + + """ + + def __init__(self, **kwargs): + # block, txid, timestamp, symbol, from, from_type, to, to_type, memo, quantity + self.raw_data = kwargs + k = kwargs.get + self.block = int(k('block', 0)) # type: int + self.txid, self.symbol, self.sender = k('txid'), k('symbol'), k('from') # type: str + self.from_type, self.to, self.to_type = k('from_type'), k('to'), k('to_type') # type: str + self.memo, self.timestamp = k('memo'), k('timestamp') # type: str + self.quantity = Decimal(k('quantity', '0')) # type: Decimal + + def __str__(self): + return f"" + + + +class SEBalance(ObjBase): + """ + Represents an account token balance on SteemEngine + + :ivar str account: The Steem username whom this balance belongs to + :ivar str symbol: The short symbol for the token held, e.g. ``ENG`` + :ivar Decimal balance: The amount of ``symbol`` that ``account`` holds. + """ + def __init__(self, account: str, symbol: str, balance: Union[Decimal, float, str], **kwargs): + self.raw_data = {**kwargs, **dict(account=account, symbol=symbol, balance=balance)} + self.account, self.symbol = account, symbol # type: str + self.balance = Decimal(balance) # type: Decimal + + def __str__(self): + return f"" + + + diff --git a/setup.py b/setup.py index d0fd8e9..3622075 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ setup( name='privex_steemengine', - version='1.0.6', + version='1.1.0', description='A small library for querying and interacting with the SteemEngine network (https://steem-engine.com)', long_description=long_description, diff --git a/tests.py b/tests.py index 28e205d..a2e8d68 100755 --- a/tests.py +++ b/tests.py @@ -40,7 +40,7 @@ def test_get_token(self): def test_get_all_balances(self): """Get all balances for user TEST_ACC, and verify that REAL_TOKEN is in there""" - res = st.get_balances(TEST_ACC) + res = list(st.get_balances(TEST_ACC)) self.assertIs(type(res), list) self.assertGreater(len(res), 0) syms = [t['symbol'].upper() for t in res]