From b7287107609c23bc884c6bbcc18de9b138cc98d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Roztoc=CC=8Cil?= Date: Sun, 4 Mar 2012 10:48:30 +0100 Subject: [PATCH] Factored out CLI parsing. --- httpie/__main__.py | 141 ++++------------------------- httpie/cli.py | 216 +++++++++++++++++++++++++++++++++++++++++++++ httpie/pretty.py | 3 +- 3 files changed, 234 insertions(+), 126 deletions(-) create mode 100644 httpie/cli.py diff --git a/httpie/__main__.py b/httpie/__main__.py index 08b2d3ff..49242d08 100644 --- a/httpie/__main__.py +++ b/httpie/__main__.py @@ -2,118 +2,17 @@ import os import sys import json -import argparse -from collections import namedtuple import requests +from collections import OrderedDict from requests.structures import CaseInsensitiveDict +from . import cli from . import pretty from . import __version__ as version -from . import __doc__ as doc DEFAULT_UA = 'HTTPie/%s' % version -SEP_COMMON = ':' -SEP_DATA = '=' -SEP_DATA_RAW = ':=' TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8' TYPE_JSON = 'application/json; charset=utf-8' -PRETTIFY_STDOUT_TTY_ONLY = object() - - -KeyValue = namedtuple('KeyValue', ['key', 'value', 'sep']) - - -class KeyValueType(object): - - def __init__(self, separators): - self.separators = separators - - def __call__(self, string): - found = dict((string.find(sep), sep) - for sep in self.separators - if string.find(sep) != -1) - - if not found: - #noinspection PyExceptionInherit - raise argparse.ArgumentTypeError( - '"%s" is not a valid value' % string) - sep = found[min(found.keys())] - key, value = string.split(sep, 1) - return KeyValue(key=key, value=value, sep=sep) - - -parser = argparse.ArgumentParser( - description=doc.strip()) - - -# Content type. -group_type = parser.add_mutually_exclusive_group(required=False) -group_type.add_argument('--json', '-j', action='store_true', - help='Serialize data items as a JSON object and set' - ' Content-Type to application/json, if not specified.') -group_type.add_argument('--form', '-f', action='store_true', - help='Serialize data items as form values and set' - ' Content-Type to application/x-www-form-urlencoded,' - ' if not specified.') - -# Output options. -parser.add_argument('--traceback', action='store_true', default=False, - help='Print a full exception traceback should one' - ' be raised by `requests`.') -group_pretty = parser.add_mutually_exclusive_group(required=False) -group_pretty.add_argument('--pretty', '-p', dest='prettify', action='store_true', - default=PRETTIFY_STDOUT_TTY_ONLY, - help='If stdout is a terminal, ' - ' the response is prettified by default (colorized and' - ' indented if it is JSON). This flag ensures' - ' prettifying even when stdout is redirected.') -group_pretty.add_argument('--ugly', '-u', help='Do not prettify the response.', - dest='prettify', action='store_false') -group_only = parser.add_mutually_exclusive_group(required=False) -group_only.add_argument('--headers', '-t', dest='print_body', - action='store_false', default=True, - help='Print only the response headers.') -group_only.add_argument('--body', '-b', dest='print_headers', - action='store_false', default=True, - help='Print only the response body.') -parser.add_argument('--style', '-s', dest='style', default='solarized', metavar='STYLE', - choices=pretty.AVAILABLE_STYLES, - help='Output coloring style, one of %s. Defaults to solarized.' - % ', '.join(sorted(pretty.AVAILABLE_STYLES))) - -# ``requests.request`` keyword arguments. -parser.add_argument('--auth', '-a', help='username:password', - type=KeyValueType(SEP_COMMON)) -parser.add_argument('--verify', - help='Set to "yes" to check the host\'s SSL certificate.' - ' You can also pass the path to a CA_BUNDLE' - ' file for private certs. You can also set ' - 'the REQUESTS_CA_BUNDLE environment variable.') -parser.add_argument('--proxy', default=[], action='append', - type=KeyValueType(SEP_COMMON), - help='String mapping protocol to the URL of the proxy' - ' (e.g. http:foo.bar:3128).') -parser.add_argument('--allow-redirects', default=False, action='store_true', - help='Set this flag if full redirects are allowed' - ' (e.g. re-POST-ing of data at new ``Location``)') -parser.add_argument('--file', metavar='PATH', type=argparse.FileType(), - default=[], action='append', - help='File to multipart upload') -parser.add_argument('--timeout', type=float, - help='Float describes the timeout of the request' - ' (Use socket.setdefaulttimeout() as fallback).') - -# Positional arguments. -parser.add_argument('method', metavar='METHOD', - help='HTTP method to be used for the request' - ' (GET, POST, PUT, DELETE, PATCH, ...).') -parser.add_argument('url', metavar='URL', - help='Protocol defaults to http:// if the' - ' URL does not include it.') -parser.add_argument('items', nargs='*', - type=KeyValueType([SEP_COMMON, SEP_DATA, SEP_DATA_RAW]), - help='HTTP header (key:value), data field (key=value)' - ' or raw JSON field (field:=value).') def main(args=None, @@ -122,33 +21,27 @@ def main(args=None, stdout=sys.stdout, stdout_isatty=sys.stdout.isatty()): + parser = cli.parser + args = parser.parse_args(args if args is not None else sys.argv[1:]) do_prettify = (args.prettify is True or - (args.prettify == PRETTIFY_STDOUT_TTY_ONLY and stdout_isatty)) + (args.prettify == cli.PRETTIFY_STDOUT_TTY_ONLY and stdout_isatty)) + # Parse request headers and data from the command line. headers = CaseInsensitiveDict() headers['User-Agent'] = DEFAULT_UA - data = {} - for item in args.items: - value = item.value - if item.sep == SEP_COMMON: - target = headers - else: - if not stdin_isatty: - parser.error('Request body (stdin) and request ' - 'data (key=value) cannot be mixed.') - if item.sep == SEP_DATA_RAW: - try: - value = json.loads(item.value) - except ValueError: - if args.traceback: - raise - parser.error('%s:=%s is not valid JSON' - % (item.key, item.value)) - target = data - target[item.key] = value + data = OrderedDict() + try: + cli.parse_items(items=args.items, headers=headers, data=data) + except cli.ParseError as e: + if args.traceback: + raise + parser.error(e.message) if not stdin_isatty: + if data: + parser.error('Request body (stdin) and request ' + 'data (key=value) cannot be mixed.') data = stdin.read() # JSON/Form content type. @@ -174,7 +67,7 @@ def main(args=None, files=dict((os.path.basename(f.name), f) for f in args.file), allow_redirects=args.allow_redirects, ) - except (KeyboardInterrupt, SystemExit) as e: + except (KeyboardInterrupt, SystemExit): sys.stderr.write('\n') sys.exit(1) except Exception as e: diff --git a/httpie/cli.py b/httpie/cli.py new file mode 100644 index 00000000..10c98a85 --- /dev/null +++ b/httpie/cli.py @@ -0,0 +1,216 @@ +import json +import argparse +from collections import namedtuple +from . import pretty +from . import __doc__ as doc +from . import __version__ as version + + +SEP_COMMON = ':' +SEP_HEADERS = SEP_COMMON +SEP_DATA = '=' +SEP_DATA_RAW_JSON = ':=' +PRETTIFY_STDOUT_TTY_ONLY = object() + + +class ParseError(Exception): + pass + + +KeyValue = namedtuple('KeyValue', ['key', 'value', 'sep', 'orig']) + + +class KeyValueType(object): + """A type used with `argparse`.""" + def __init__(self, *separators): + self.separators = separators + + def __call__(self, string): + found = dict((string.find(sep), sep) + for sep in self.separators + if string.find(sep) != -1) + + if not found: + #noinspection PyExceptionInherit + raise argparse.ArgumentTypeError( + '"%s" is not a valid value' % string) + sep = found[min(found.keys())] + key, value = string.split(sep, 1) + return KeyValue(key=key, value=value, sep=sep, orig=string) + + +def parse_items(items, data=None, headers=None): + """Parse `KeyValueType` `items` into `data` and `headers`.""" + if headers is None: + headers = {} + if data is None: + data = {} + for item in items: + value = item.value + if item.sep == SEP_HEADERS: + target = headers + elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]: + if item.sep == SEP_DATA_RAW_JSON: + try: + value = json.loads(item.value) + except ValueError: + raise ParseError('%s is not valid JSON' % item.orig) + target = data + else: + raise ParseError('%s is not valid item' % item.orig) + + if item.key in target: + ParseError('duplicate item %s (%s)' % (item.key, item.orig)) + + target[item.key] = value + + return headers, data + + +def _(text): + """Normalize white space.""" + return ' '.join(text.strip().split()) + + +parser = argparse.ArgumentParser(description=doc.strip(),) + +# Content type. +############################################# + +group_type = parser.add_mutually_exclusive_group(required=False) +group_type.add_argument( + '--json', '-j', action='store_true', + help=_(''' + Serialize data items as a JSON object and set + Content-Type to application/json, if not specified. + ''') +) +group_type.add_argument( + '--form', '-f', action='store_true', + help=_(''' + Serialize data items as form values and set + Content-Type to application/x-www-form-urlencoded, + if not specified. + ''') +) + + +# Output options. +############################################# + +parser.add_argument( + '--traceback', action='store_true', default=False, + help=_(''' + Print exception traceback should one occur. + ''') +) + +prettify = parser.add_mutually_exclusive_group(required=False) +prettify.add_argument( + '--pretty', '-p', dest='prettify', action='store_true', + default=PRETTIFY_STDOUT_TTY_ONLY, + help=_(''' + If stdout is a terminal, + the response is prettified by default (colorized and + indented if it is JSON). This flag ensures + prettifying even when stdout is redirected. + ''') +) +prettify.add_argument( + '--ugly', '-u', dest='prettify', action='store_false', + help=_(''' + Do not prettify the response. + ''') +) + +only = parser.add_mutually_exclusive_group(required=False) +only.add_argument( + '--headers', '-t', dest='print_body', + action='store_false', default=True, + help=(''' + Print only the response headers. + ''') +) +only.add_argument( + '--body', '-b', dest='print_headers', + action='store_false', default=True, + help=(''' + Print only the response body. + ''') +) +parser.add_argument( + '--style', '-s', dest='style', default='solarized', metavar='STYLE', + choices=pretty.AVAILABLE_STYLES, + help=_(''' + Output coloring style, one of %s. Defaults to solarized. + ''') % ', '.join(sorted(pretty.AVAILABLE_STYLES)) +) + +# ``requests.request`` keyword arguments. +parser.add_argument( + '--auth', '-a', help='username:password', + type=KeyValueType(SEP_COMMON) +) +parser.add_argument( + '--verify', + help=_(''' + Set to "yes" to check the host\'s SSL certificate. + You can also pass the path to a CA_BUNDLE + file for private certs. You can also set + the REQUESTS_CA_BUNDLE environment variable. + ''') +) +parser.add_argument( + '--proxy', default=[], action='append', + type=KeyValueType(SEP_COMMON), + help=_(''' + String mapping protocol to the URL of the proxy + (e.g. http:foo.bar:3128). + ''') +) +parser.add_argument( + '--allow-redirects', default=False, action='store_true', + help=_(''' + Set this flag if full redirects are allowed + (e.g. re-POST-ing of data at new ``Location``) + ''') +) +parser.add_argument( + '--file', metavar='PATH', type=argparse.FileType(), + default=[], action='append', + help='File to multipart upload' +) +parser.add_argument( + '--timeout', type=float, + help=_(''' + Float describes the timeout of the request + (Use socket.setdefaulttimeout() as fallback). + ''') +) + + +# Positional arguments. +############################################# + +parser.add_argument( + 'method', metavar='METHOD', + help=_(''' + HTTP method to be used for the request + (GET, POST, PUT, DELETE, PATCH, ...). + ''') +) +parser.add_argument( + 'url', metavar='URL', + help=_(''' + Protocol defaults to http:// if the + URL does not include it. + ''') +) +parser.add_argument( + 'items', nargs='*', + type=KeyValueType(SEP_COMMON, SEP_DATA, SEP_DATA_RAW_JSON), + help=_(''' + HTTP header (key:value), data field (key=value) + or raw JSON field (field:=value). + ''') +) diff --git a/httpie/pretty.py b/httpie/pretty.py index ae661cbd..3e4fea09 100644 --- a/httpie/pretty.py +++ b/httpie/pretty.py @@ -1,4 +1,3 @@ -import re import os import json import pygments @@ -16,7 +15,7 @@ DEFAULT_STYLE = 'solarized' AVAILABLE_STYLES = [DEFAULT_STYLE] + STYLE_MAP.keys() TYPE_JS = 'application/javascript' FORMATTER = (Terminal256Formatter - if os.environ.get('TERM', None) == 'xterm-256color' + if os.environ.get('TERM') == 'xterm-256color' else TerminalFormatter)