1
0
mirror of https://github.com/httpie/cli.git synced 2025-04-23 12:08:50 +02:00

Factored out CLI parsing.

This commit is contained in:
Jakub Roztočil 2012-03-04 10:48:30 +01:00
parent 715e1b1047
commit b728710760
3 changed files with 234 additions and 126 deletions

View File

@ -2,118 +2,17 @@
import os import os
import sys import sys
import json import json
import argparse
from collections import namedtuple
import requests import requests
from collections import OrderedDict
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from . import cli
from . import pretty from . import pretty
from . import __version__ as version from . import __version__ as version
from . import __doc__ as doc
DEFAULT_UA = 'HTTPie/%s' % version DEFAULT_UA = 'HTTPie/%s' % version
SEP_COMMON = ':'
SEP_DATA = '='
SEP_DATA_RAW = ':='
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8' TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
TYPE_JSON = 'application/json; 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, def main(args=None,
@ -122,33 +21,27 @@ def main(args=None,
stdout=sys.stdout, stdout=sys.stdout,
stdout_isatty=sys.stdout.isatty()): stdout_isatty=sys.stdout.isatty()):
parser = cli.parser
args = parser.parse_args(args if args is not None else sys.argv[1:]) args = parser.parse_args(args if args is not None else sys.argv[1:])
do_prettify = (args.prettify is True or 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. # Parse request headers and data from the command line.
headers = CaseInsensitiveDict() headers = CaseInsensitiveDict()
headers['User-Agent'] = DEFAULT_UA headers['User-Agent'] = DEFAULT_UA
data = {} data = OrderedDict()
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: try:
value = json.loads(item.value) cli.parse_items(items=args.items, headers=headers, data=data)
except ValueError: except cli.ParseError as e:
if args.traceback: if args.traceback:
raise raise
parser.error('%s:=%s is not valid JSON' parser.error(e.message)
% (item.key, item.value))
target = data
target[item.key] = value
if not stdin_isatty: if not stdin_isatty:
if data:
parser.error('Request body (stdin) and request '
'data (key=value) cannot be mixed.')
data = stdin.read() data = stdin.read()
# JSON/Form content type. # 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), files=dict((os.path.basename(f.name), f) for f in args.file),
allow_redirects=args.allow_redirects, allow_redirects=args.allow_redirects,
) )
except (KeyboardInterrupt, SystemExit) as e: except (KeyboardInterrupt, SystemExit):
sys.stderr.write('\n') sys.stderr.write('\n')
sys.exit(1) sys.exit(1)
except Exception as e: except Exception as e:

216
httpie/cli.py Normal file
View File

@ -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).
''')
)

View File

@ -1,4 +1,3 @@
import re
import os import os
import json import json
import pygments import pygments
@ -16,7 +15,7 @@ DEFAULT_STYLE = 'solarized'
AVAILABLE_STYLES = [DEFAULT_STYLE] + STYLE_MAP.keys() AVAILABLE_STYLES = [DEFAULT_STYLE] + STYLE_MAP.keys()
TYPE_JS = 'application/javascript' TYPE_JS = 'application/javascript'
FORMATTER = (Terminal256Formatter FORMATTER = (Terminal256Formatter
if os.environ.get('TERM', None) == 'xterm-256color' if os.environ.get('TERM') == 'xterm-256color'
else TerminalFormatter) else TerminalFormatter)