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

Added AcctThread.py utility: #1

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

scottschurr
Copy link

AcctThread.py is a command line utility that takes a ripple account ID as an argument then walks the PreviousTxnID thread left behind in the account root. This walk will, if enough history is available, pass through all transactions affecting the account root in question until we arrive at the creation of the account root.

Output is JSON (with text separators and text comments) to stdout. To capture the output to a file redirect the output.

AcctThread is a command line utility that takes a ripple account
ID as an argument then walks the PreviousTxnID thread left
behind in the account root.  This walk will, if enough history
is available, pass through all transactions affecting the
account root in question until we arrive at the creation of the
account root.

Output is JSON (with text separators and text comments) to
stdout.  To capture the output to a file redirect the output.
Copy link
Contributor

@seelabs seelabs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainlly not a python expert, but I did a quick pass.

AcctThread.py Outdated
@@ -0,0 +1,280 @@
#!/usr/local/bin/python3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#!/usr/bin/env python3 is better. python3 doesn't always live in /usr/local/bin

AcctThread.py Outdated
from websocket import create_connection

# Extract command line arguments.
def extractArgs ():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't put a space between a function and the argument parens. The standard for formatting python code is pep8. See this section: https://www.python.org/dev/peps/pep-0008/#whitespace-in-expressions-and-statements

I recommend using an automatic formatter for python code. I use yapf (https://github.com/google/yapf) but there are other good formatters out there as well.

AcctThread.py Outdated
connectTo = "ws://s2.ripple.com:443"

usage = 'usage: PyAcctThread <Account ID> [ws://<Server>:<port>]\n'\
'If <server>:<port> are omitted defaults to "{0}"'.format (connectTo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 3.6 introduced f strings (see https://www.python.org/dev/peps/pep-0498/). I strongly prefer them for my own scripts, but of course I don't have to worry about supporting multiple python interperters. Your call if you want to support python 3.5 or earlier.

AcctThread.py Outdated

from websocket import create_connection

# Extract command line arguments.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a comment at the top of a function, this should be a docstring. I.e.

def extractArgs():
    '''Extrace command line arguments'''

AcctThread.py Outdated
def wsId ():
global wsIdValue
wsIdValue += 1
return wsIdValue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making wsIdValue global I'd make it an attribute of the function. I.e

def wsId():
    wsId.value += 1
    return wsId.value
wsId.value = 0

AcctThread.py Outdated
msg = ws.recv ()
jsonMsg = json.loads (msg)
gotId = jsonMsg["id"]
if (gotId == id):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python if statements don't need parens.

Copy link
Contributor

@seelabs seelabs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't put this script in the root directory. We should find or create a subdirectory for it.

AcctThread.py Outdated
def extractArgs ():

# Default websocket connection if none provided.
connectTo = "ws://s2.ripple.com:443"
Copy link
Contributor

@seelabs seelabs Aug 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function and variable names should be snake case. see: https://www.python.org/dev/peps/pep-0008/#function-and-variable-names

AcctThread.py Outdated


# Used to track websocket requests.
wsIdValue = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

global variable should be upper case separated by underscores. I.e WS_ID_VALUE, though see the comment about making this a attribute of the function rather than a global.

AcctThread.py Outdated

# Get the websocket response that matches the id of the request. All
# responses are in json, so return json.
def getResponse (ws, id):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id is a builtin. It's better not to use it as a parameter name.

AcctThread.py Outdated

if __name__ == "__main__":
# Get command line arguments.
connectTo, accountId = extractArgs ()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are global variables. Line 34 has a local variable that shadows this global. There's a couple ways to fix this:

  1. If you want to keep them global, rename them to use the global convention of UPPER_CASE
  2. Create a function main make the variables local
  3. Call the function directly with the tuple returned from extractArgs. I.e. openConnection(*extractArgs())

AcctThread.py Outdated
return ""

err = jsonMsg["error"]
if jsonMsg[error_message]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong. Did you mean 'error_message'? (and below)

AcctThread.py Outdated
idLen = len (accountId)
if (accountId[:1] != "r") or (idLen < 25) or (idLen > 35):
print ('Invalid format for account ID.\n'\
'Should start with "r" with length between 25 and 35 characters.\n')
Copy link
Contributor

@seelabs seelabs Aug 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a line continuation here (and other print statements), I'd pass multiple arguments into print. I.e.

print('long_line_1', 
        'print_long_line_2')

AcctThread.py Outdated
# the server does not have enough history.
if jsonMsg["error"] == "txnNotFound":
print ("Transaction not found. "
"Does your server have enough history? Unexpected stop.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd single space between sentences

@seelabs
Copy link
Contributor

seelabs commented Aug 15, 2018

Here's a commit with all my feedback in one place (note, I'm unable to test this locally right now; I have a package conflict between websock and vega, so I can't install websocket): seelabs@ee51bc9

@scottschurr
Copy link
Author

@seelabs, thanks for all the help. I cherry-picked your feedback and made a few other changes I hope are improvements.

@seelabs
Copy link
Contributor

seelabs commented Aug 15, 2018

@scottschurr I was more thinking about a "scripts" directory or somesuch to put these standalone scripts. Other than that LGTM. 👍 (and still 👍 if you want to keep the script in the dir you already put it in).

Copy link
Collaborator

@wilsonianb wilsonianb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works 🎉



def print_tx(ws, txn_id):
'''Request tx by tnx_id, print it, and return the tx as JSON'''
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tnx_id -> txn_id

json_msg = get_response(ws, wsid)

# Remove the websocket id to reduce noise.
json_msg.pop("id", None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could "id" be removed inside get_response?
We seem to never want it, and get_response already checks that "id" matches req_id/wsid

this account.
3. Call tx with that TxId. Save that transaction.
4. The tx response should contain the AccountRoot for the account_id.
Extract the new value of PreviousTxID from the AccountRoot.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PreviousTxID -> PreviousTxnID
here and in 5 below


err = json_msg["error"]
if json_msg['error_message']:
err = json_msg['error_message']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be

err = json_msg['error_message'] or json_msg["error"]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that would work if my original code were correct. Which it's not. I had failed to check that error handling path. The old code did not work because a missing key in a Dictionary throws a KeyError exception rather than return None. Fortunately your nudge led me to actually test that error condition. So now I've changed the code to this:

        if "error_message" in json_msg:
            err = json_msg["error_message"]
        else:
            err = json_msg["error"] + "."

I think that's easier to read than the old code anyway. And I also tested both legs this time 🙂.

except KeyError:
pass # If the field is not found that's okay.

# Else look for the next transaction.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could check for the next transaction first, so that we only have to do the "Else" (check for account creation) once per account.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have a fair point that we'll handle slightly fewer exceptions if we check for the "ModifiedNode" case first, since that's more common. But any node that doesn't meet either of the criteria will receive both checks (and throw both exceptions).

Unfortunately I didn't find an attractive way to look for the presence of nested dictionary entries that doesn't involve KeyError exceptions. And since this is not performance critical code I decided readability was more important than avoiding the exceptions.

import sys
import websocket

from websocket import create_connection
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be helpful to note somewhere that this script needs websocket-client installed

connect_to = sys.argv[2]

# Validate the connect_to.
if connect_to[:5] != "ws://":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not allow wss:// (and use that for the default)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. That seems to work. When I was first cobbling this together I was having problems with wss:// so I switch to ws:// which worked. But I'm not seeing any issues at the moment, so supporting both seems fine.

Copy link
Author

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the excellent comments, @wilsonianb!

except KeyError:
pass # If the field is not found that's okay.

# Else look for the next transaction.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have a fair point that we'll handle slightly fewer exceptions if we check for the "ModifiedNode" case first, since that's more common. But any node that doesn't meet either of the criteria will receive both checks (and throw both exceptions).

Unfortunately I didn't find an attractive way to look for the presence of nested dictionary entries that doesn't involve KeyError exceptions. And since this is not performance critical code I decided readability was more important than avoiding the exceptions.

connect_to = sys.argv[2]

# Validate the connect_to.
if connect_to[:5] != "ws://":
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. That seems to work. When I was first cobbling this together I was having problems with wss:// so I switch to ws:// which worked. But I'm not seeing any issues at the moment, so supporting both seems fine.


err = json_msg["error"]
if json_msg['error_message']:
err = json_msg['error_message']
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that would work if my original code were correct. Which it's not. I had failed to check that error handling path. The old code did not work because a missing key in a Dictionary throws a KeyError exception rather than return None. Fortunately your nudge led me to actually test that error condition. So now I've changed the code to this:

        if "error_message" in json_msg:
            err = json_msg["error_message"]
        else:
            err = json_msg["error"] + "."

I think that's easier to read than the old code anyway. And I also tested both legs this time 🙂.

Copy link
Collaborator

@wilsonianb wilsonianb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍
Thanks for being a "First-time contributor" 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants