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]