blob: 1d5384cdd88b463fa5fa83e5f1a4d7c60ca3aab7 [file] [log] [blame]
# -*- coding: utf-8 -*-
"""
hyper/cli
~~~~~~~~~
Command line interface for Hyper inspired by Httpie.
"""
import json
import locale
import logging
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from argparse import OPTIONAL, ZERO_OR_MORE
from pprint import pformat
from textwrap import dedent
from hyper import HTTPConnection, HTTP20Connection
from hyper import __version__
from hyper.compat import is_py2, urlencode, urlsplit, write_to_stdout
from hyper.common.util import to_host_port_tuple
log = logging.getLogger('hyper')
PREFERRED_ENCODING = locale.getpreferredencoding()
# Various separators used in args
SEP_HEADERS = ':'
SEP_QUERY = '=='
SEP_DATA = '='
SEP_GROUP_ITEMS = [
SEP_HEADERS,
SEP_QUERY,
SEP_DATA,
]
class KeyValue(object):
"""Base key-value pair parsed from CLI."""
def __init__(self, key, value, sep, orig):
self.key = key
self.value = value
self.sep = sep
self.orig = orig
class KeyValueArgType(object):
"""A key-value pair argument type used with `argparse`.
Parses a key-value arg and constructs a `KeyValue` instance.
Used for headers, form data, and other key-value pair types.
This class is inspired by httpie and implements simple tokenizer only.
"""
def __init__(self, *separators):
self.separators = separators
def __call__(self, string):
for sep in self.separators:
splitted = string.split(sep, 1)
if len(splitted) == 2:
key, value = splitted
return KeyValue(key, value, sep, string)
def make_positional_argument(parser):
parser.add_argument(
'method', metavar='METHOD', nargs=OPTIONAL, default='GET',
help=dedent("""
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, ...).
"""))
parser.add_argument(
'_url', metavar='URL',
help=dedent("""
The scheme defaults to 'https://' if the URL does not include one.
"""))
parser.add_argument(
'items',
metavar='REQUEST_ITEM',
nargs=ZERO_OR_MORE,
type=KeyValueArgType(*SEP_GROUP_ITEMS),
help=dedent("""
Optional key-value pairs to be included in the request.
The separator used determines the type:
':' HTTP headers:
Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
'==' URL parameters to be appended to the request URI:
search==hyper
'=' Data fields to be serialized into a JSON object:
name=Hyper language=Python description='CLI HTTP client'
"""))
def make_troubleshooting_argument(parser):
parser.add_argument(
'--version', action='version', version=__version__,
help='Show version and exit.')
parser.add_argument(
'--debug', action='store_true', default=False,
help='Show debugging information (loglevel=DEBUG)')
parser.add_argument(
'--h2', action='store_true', default=False,
help="Do HTTP/2 directly, skipping plaintext upgrade and ignoring "
"NPN/ALPN."
)
def split_host_and_port(hostname):
if ':' in hostname:
return to_host_port_tuple(hostname, default_port=443)
return hostname, None
class UrlInfo(object):
def __init__(self):
self.fragment = None
self.host = 'localhost'
self.netloc = None
self.path = '/'
self.port = 443
self.query = None
self.scheme = 'https'
self.secure = False
def set_url_info(args):
info = UrlInfo()
_result = urlsplit(args._url)
for attr in vars(info).keys():
value = getattr(_result, attr, None)
if value:
setattr(info, attr, value)
if info.scheme == 'http' and not _result.port:
info.port = 80
# Set the secure arg is the scheme is HTTPS, otherwise do unsecured.
info.secure = info.scheme == 'https'
if info.netloc:
hostname, _ = split_host_and_port(info.netloc)
info.host = hostname # ensure stripping port number
else:
if _result.path:
_path = _result.path.split('/', 1)
hostname, port = split_host_and_port(_path[0])
info.host = hostname
if info.path == _path[0]:
info.path = '/'
elif len(_path) == 2 and _path[1]:
info.path = '/' + _path[1]
if port is not None:
info.port = port
log.debug('Url Info: %s', vars(info))
args.url = info
def set_request_data(args):
body, headers, params = {}, {}, {}
for i in args.items:
if i.sep == SEP_HEADERS:
if i.key:
headers[i.key] = i.value
else:
# when overriding a HTTP/2 special header there will be a
# leading colon, which tricks the command line parser into
# thinking the header is empty
k, v = i.value.split(':', 1)
headers[':' + k] = v
elif i.sep == SEP_QUERY:
params[i.key] = i.value
elif i.sep == SEP_DATA:
value = i.value
if is_py2: # pragma: no cover
value = value.decode(PREFERRED_ENCODING)
body[i.key] = value
if params:
args.url.path += '?' + urlencode(params)
if body:
content_type = 'application/json'
headers.setdefault('content-type', content_type)
args.body = json.dumps(body)
if args.method is None:
args.method = 'POST' if args.body else 'GET'
args.method = args.method.upper()
args.headers = headers
def parse_argument(argv=None):
parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
parser.set_defaults(body=None, headers={})
make_positional_argument(parser)
make_troubleshooting_argument(parser)
args = parser.parse_args(sys.argv[1:] if argv is None else argv)
if args.debug:
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
log.addHandler(handler)
log.setLevel(logging.DEBUG)
set_url_info(args)
set_request_data(args)
return args
def get_content_type_and_charset(response):
charset = 'utf-8'
content_type = response.headers.get('content-type')
if content_type is None:
return 'unknown', charset
content_type = content_type[0].decode('utf-8').lower()
type_and_charset = content_type.split(';', 1)
ctype = type_and_charset[0].strip()
if len(type_and_charset) == 2:
charset = type_and_charset[1].strip().split('=')[1]
return ctype, charset
def request(args):
if not args.h2:
conn = HTTPConnection(
args.url.host, args.url.port, secure=args.url.secure
)
else: # pragma: no cover
conn = HTTP20Connection(
args.url.host,
args.url.port,
secure=args.url.secure,
force_proto='h2'
)
conn.request(args.method, args.url.path, args.body, args.headers)
response = conn.get_response()
log.debug('Response Headers:\n%s', pformat(response.headers))
ctype, charset = get_content_type_and_charset(response)
data = response.read()
return data
def main(argv=None):
args = parse_argument(argv)
log.debug('Commandline Argument: %s', args)
data = request(args)
write_to_stdout(data)
if __name__ == '__main__': # pragma: no cover
main()