Skip to content
This repository has been archived by the owner on Jan 13, 2021. It is now read-only.

Commit

Permalink
Merge pull request #82 from t2y/add-cli-tool
Browse files Browse the repository at this point in the history
Add hyper as a Command Line Interface
  • Loading branch information
Lukasa committed Feb 11, 2015
2 parents 26ae40d + b47a0e8 commit 7ae118c
Show file tree
Hide file tree
Showing 4 changed files with 452 additions and 2 deletions.
231 changes: 231 additions & 0 deletions hyper/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
"""
hyper/cli
~~~~~~~~~
Command line interface for Hyper inspired by Httpie.
"""
import json
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 HTTP20Connection
from hyper import __version__
from hyper.compat import urlencode, urlsplit


log = logging.getLogger('hyper')

FILESYSTEM_ENCODING = sys.getfilesystemencoding()

# 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)')


def set_url_info(args):
def split_host_and_port(hostname):
if ':' in hostname:
host, port = hostname.split(':')
return host, int(port)
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'

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

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:
headers[i.key] = i.value
elif i.sep == SEP_QUERY:
params[i.key] = i.value
elif i.sep == SEP_DATA:
body[i.key] = i.value

if params:
args.url.path += '?' + urlencode(params)

if body:
content_type = 'application/json; charset=%s' % FILESYSTEM_ENCODING
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.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.getheader('content-type')
if content_type is None:
return 'unknown', charset

content_type = content_type.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):
conn = HTTP20Connection(args.url.host, args.url.port)
conn.request(args.method, args.url.path, args.body, args.headers)
response = conn.getresponse()
log.debug('Response Headers:\n%s', pformat(response.getheaders()))
ctype, charset = get_content_type_and_charset(response)
data = response.read().decode(charset)
if 'json' in ctype:
data = pformat(json.loads(data))
return data


def main(argv=None):
args = parse_argument(argv)
log.debug('Commandline Argument: %s', args)
print(request(args))


if __name__ == '__main__': # pragma: no cover
main()
5 changes: 3 additions & 2 deletions hyper/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def ignore_missing():
else:
ssl = ssl_compat

from urlparse import urlparse
from urllib import urlencode
from urlparse import urlparse, urlsplit

def to_byte(char):
return ord(char)
Expand All @@ -48,7 +49,7 @@ def zlib_compressobj(level=6, method=zlib.DEFLATED, wbits=15, memlevel=8,
return zlib.compressobj(level, method, wbits, memlevel, strategy)

elif is_py3:
from urllib.parse import urlparse
from urllib.parse import urlencode, urlparse, urlsplit

def to_byte(char):
return char
Expand Down
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ def resolve_install_requires():
'Programming Language :: Python :: Implementation :: CPython',
],
install_requires=resolve_install_requires(),
entry_points={
'console_scripts': [
'hyper = hyper.cli:main',
],
},
)

0 comments on commit 7ae118c

Please sign in to comment.