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

Change optparse for argparse. #238

Merged
merged 24 commits into from
May 19, 2017
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ omit =
.tox/*
setup.py
*.egg/*
*/__main__.py

1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ env:
- TOXENV=py35-contrib_crypto
- TOXENV=py36-contrib_crypto
- TOXENV=py27-contrib_crypto

install:
- pip install -U pip
- pip install -U tox coveralls
Expand Down
227 changes: 132 additions & 95 deletions jwt/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,172 @@

from __future__ import absolute_import, print_function

import argparse
import json
import optparse
import sys
import time

from . import DecodeError, __package__, __version__, decode, encode
from . import DecodeError, __version__, decode, encode


def main():
def encode_payload(args):
# Try to encode
if args.key is None:
raise ValueError('Key is required when encoding. See --help for usage.')

usage = '''Encodes or decodes JSON Web Tokens based on input.
# Build payload object to encode
payload = {}

%prog [options] input
for arg in args.payload:
try:
k, v = arg.split('=', 1)

# exp +offset special case?
if k == 'exp' and v[0] == '+' and len(v) > 1:
v = str(int(time.time()+int(v[1:])))

# Cast to integer?
if v.isdigit():
v = int(v)
else:
# Cast to float?
try:
v = float(v)
except ValueError:
pass

# Cast to true, false, or null?
constants = {'true': True, 'false': False, 'null': None}

if v in constants:
v = constants[v]

payload[k] = v
except ValueError:
raise ValueError('Invalid encoding input at %s' % arg)

try:
token = encode(
payload,
key=args.key,
algorithm=args.algorithm
)

return token.decode('utf-8')
except Exception as e:
raise Exception(e)


def decode_payload(args):
try:
if sys.stdin.isatty():
token = sys.stdin.read()
else:
token = args.token

token = token.encode('utf-8')
data = decode(token, key=args.key, verify=args.verify)

Decoding examples:
return json.dumps(data)

%prog --key=secret json.web.token
%prog --no-verify json.web.token
except DecodeError as e:
raise DecodeError('There was an error decoding the token: %s' % e)

Encoding requires the key option and takes space separated key/value pairs
separated by equals (=) as input. Examples:

%prog --key=secret iss=me exp=1302049071
%prog --key=secret foo=bar exp=+10
def build_argparser():

The exp key is special and can take an offset to current Unix time.\
'''
p = optparse.OptionParser(
usage=usage,
usage = '''
Encodes or decodes JSON Web Tokens based on input.

%(prog)s [options] <command> [options] input

Decoding examples:

%(prog)s --key=secret decode json.web.token
%(prog)s decode --no-verify json.web.token

Encoding requires the key option and takes space separated key/value pairs
separated by equals (=) as input. Examples:

%(prog)s --key=secret encode iss=me exp=1302049071
%(prog)s --key=secret encode foo=bar exp=+10

The exp key is special and can take an offset to current Unix time.
'''

arg_parser = argparse.ArgumentParser(
prog='pyjwt',
version='%s %s' % (__package__, __version__),
usage=usage
)

p.add_option(
'-n', '--no-verify',
action='store_false',
dest='verify',
default=True,
help='ignore signature and claims verification on decode'
arg_parser.add_argument(
'-v', '--version',
action='version',
version='%(prog)s ' + __version__
)

p.add_option(
arg_parser.add_argument(
'--key',
dest='key',
metavar='KEY',
default=None,
help='set the secret key to sign with'
)

p.add_option(
arg_parser.add_argument(
'--alg',
dest='algorithm',
metavar='ALG',
default='HS256',
help='set crypto algorithm to sign with. default=HS256'
)

options, arguments = p.parse_args()

if len(arguments) > 0 or not sys.stdin.isatty():
if len(arguments) == 1 and (not options.verify or options.key):
# Try to decode
try:
if not sys.stdin.isatty():
token = sys.stdin.read()
else:
token = arguments[0]

token = token.encode('utf-8')
data = decode(token, key=options.key, verify=options.verify)

print(json.dumps(data))
sys.exit(0)
except DecodeError as e:
print(e)
sys.exit(1)

# Try to encode
if options.key is None:
print('Key is required when encoding. See --help for usage.')
sys.exit(1)

# Build payload object to encode
payload = {}

for arg in arguments:
try:
k, v = arg.split('=', 1)

# exp +offset special case?
if k == 'exp' and v[0] == '+' and len(v) > 1:
v = str(int(time.time()+int(v[1:])))

# Cast to integer?
if v.isdigit():
v = int(v)
else:
# Cast to float?
try:
v = float(v)
except ValueError:
pass

# Cast to true, false, or null?
constants = {'true': True, 'false': False, 'null': None}

if v in constants:
v = constants[v]

payload[k] = v
except ValueError:
print('Invalid encoding input at {}'.format(arg))
sys.exit(1)
subparsers = arg_parser.add_subparsers(
title='PyJWT subcommands',
description='valid subcommands',
help='additional help'
)

try:
token = encode(
payload,
key=options.key,
algorithm=options.algorithm
)
# Encode subcommand
encode_parser = subparsers.add_parser('encode', help='use to encode a supplied payload')

payload_help = """Payload to encode. Must be a space separated list of key/value
pairs separated by equals (=) sign."""

encode_parser.add_argument('payload', nargs='+', help=payload_help)
encode_parser.set_defaults(func=encode_payload)

# Decode subcommand
decode_parser = subparsers.add_parser('decode', help='use to decode a supplied JSON web token')
decode_parser.add_argument('token', help='JSON web token to decode.')

decode_parser.add_argument(
'-n', '--no-verify',
action='store_false',
dest='verify',
default=True,
help='ignore signature and claims verification on decode'
)

decode_parser.set_defaults(func=decode_payload)

print(token)
sys.exit(0)
except Exception as e:
print(e)
sys.exit(1)
else:
p.print_help()
return arg_parser


def main(args):
arg_parser = build_argparser()

try:
arguments = arg_parser.parse_args(args)

return arguments.func(arguments)
except Exception as e:
print('There was an unforseen error: ', e)
arg_parser.print_help()
sys.exit(1)


if __name__ == '__main__':
Copy link
Owner

Choose a reason for hiding this comment

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

@froi Noticed another thing missing.

  1. Run python setup.py install
  2. Run pyjwt

It'll fail with something like this:

$ pyjwt
Traceback (most recent call last):
  File "/Users/jpadilla/.pyenv/versions/pyjwt/bin/pyjwt", line 9, in <module>
    load_entry_point('PyJWT==1.5.0', 'console_scripts', 'pyjwt')()
TypeError: main() takes exactly 1 argument (0 given)

Since we're using the console_scripts entry point on our setup.py I think we should be able to drop just drop the if __name__ == '__main__': ... and have the main() method optionally set args to sys.argv[1:]. Let me know if this makes sense.

Copy link
Contributor Author

@froi froi May 18, 2017

Choose a reason for hiding this comment

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

That'll work but it'll also make main() harder to test I think. I'll take a look at it.

main()
output = main(sys.argv[1:])

print(output)
60 changes: 60 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

froi marked this conversation as resolved.
Show resolved Hide resolved
from __future__ import unicode_literals

import jwt
from jwt.__main__ import build_argparser, decode_payload, encode_payload, main

import pytest


class TestCli:

def test_build_argparse(self):
args = ['--key', '1234', 'encode', 'name=Vader']
parser = build_argparser()
parsed_args = parser.parse_args(args)

assert parsed_args.key == '1234'

def test_encode_payload_raises_value_error_key_is_required(self):
encode_args = ['encode', 'name=Vader', 'job=Sith']
parser = build_argparser()

args = parser.parse_args(encode_args)

with pytest.raises(ValueError) as excinfo:
encode_payload(args)

assert 'Key is required when encoding' in str(excinfo.value)

def test_decode_payload_raises_decoded_error(self):
decode_args = ['--key', '1234', 'decode', 'wrong-token']
parser = build_argparser()

args = parser.parse_args(decode_args)

with pytest.raises(jwt.DecodeError) as excinfo:
decode_payload(args)

assert 'There was an error decoding the token' in str(excinfo.value)

@pytest.mark.parametrize('key,name,job', [
('1234', 'Vader', 'Sith'),
('4567', 'Anakin', 'Jedi'),
])
def test_main_run(self, key, name, job):
args = [
froi marked this conversation as resolved.
Show resolved Hide resolved
'--key', key,
'encode',
'name={0}'.format(name),
'job={0}'.format(job)
]

token = main(args)
actual = jwt.decode(token, key)
expected = {
'job': job,
'name': name,
}

assert actual == expected