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

Commit 7ae118c

Browse files
committed
Merge pull request #82 from t2y/add-cli-tool
Add hyper as a Command Line Interface
2 parents 26ae40d + b47a0e8 commit 7ae118c

File tree

4 files changed

+452
-2
lines changed

4 files changed

+452
-2
lines changed

hyper/cli.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
hyper/cli
4+
~~~~~~~~~
5+
6+
Command line interface for Hyper inspired by Httpie.
7+
"""
8+
import json
9+
import logging
10+
import sys
11+
from argparse import ArgumentParser, RawTextHelpFormatter
12+
from argparse import OPTIONAL, ZERO_OR_MORE
13+
from pprint import pformat
14+
from textwrap import dedent
15+
16+
from hyper import HTTP20Connection
17+
from hyper import __version__
18+
from hyper.compat import urlencode, urlsplit
19+
20+
21+
log = logging.getLogger('hyper')
22+
23+
FILESYSTEM_ENCODING = sys.getfilesystemencoding()
24+
25+
# Various separators used in args
26+
SEP_HEADERS = ':'
27+
SEP_QUERY = '=='
28+
SEP_DATA = '='
29+
30+
SEP_GROUP_ITEMS = [
31+
SEP_HEADERS,
32+
SEP_QUERY,
33+
SEP_DATA,
34+
]
35+
36+
37+
class KeyValue(object):
38+
"""Base key-value pair parsed from CLI."""
39+
40+
def __init__(self, key, value, sep, orig):
41+
self.key = key
42+
self.value = value
43+
self.sep = sep
44+
self.orig = orig
45+
46+
47+
class KeyValueArgType(object):
48+
"""A key-value pair argument type used with `argparse`.
49+
50+
Parses a key-value arg and constructs a `KeyValue` instance.
51+
Used for headers, form data, and other key-value pair types.
52+
This class is inspired by httpie and implements simple tokenizer only.
53+
"""
54+
def __init__(self, *separators):
55+
self.separators = separators
56+
57+
def __call__(self, string):
58+
for sep in self.separators:
59+
splitted = string.split(sep, 1)
60+
if len(splitted) == 2:
61+
key, value = splitted
62+
return KeyValue(key, value, sep, string)
63+
64+
65+
def make_positional_argument(parser):
66+
parser.add_argument(
67+
'method', metavar='METHOD', nargs=OPTIONAL, default='GET',
68+
help=dedent("""
69+
The HTTP method to be used for the request
70+
(GET, POST, PUT, DELETE, ...).
71+
"""))
72+
parser.add_argument(
73+
'_url', metavar='URL',
74+
help=dedent("""
75+
The scheme defaults to 'https://' if the URL does not include one.
76+
"""))
77+
parser.add_argument(
78+
'items',
79+
metavar='REQUEST_ITEM',
80+
nargs=ZERO_OR_MORE,
81+
type=KeyValueArgType(*SEP_GROUP_ITEMS),
82+
help=dedent("""
83+
Optional key-value pairs to be included in the request.
84+
The separator used determines the type:
85+
86+
':' HTTP headers:
87+
88+
Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
89+
90+
'==' URL parameters to be appended to the request URI:
91+
92+
search==hyper
93+
94+
'=' Data fields to be serialized into a JSON object:
95+
96+
name=Hyper language=Python description='CLI HTTP client'
97+
"""))
98+
99+
100+
def make_troubleshooting_argument(parser):
101+
parser.add_argument(
102+
'--version', action='version', version=__version__,
103+
help='Show version and exit.')
104+
parser.add_argument(
105+
'--debug', action='store_true', default=False,
106+
help='Show debugging information (loglevel=DEBUG)')
107+
108+
109+
def set_url_info(args):
110+
def split_host_and_port(hostname):
111+
if ':' in hostname:
112+
host, port = hostname.split(':')
113+
return host, int(port)
114+
return hostname, None
115+
116+
class UrlInfo(object):
117+
def __init__(self):
118+
self.fragment = None
119+
self.host = 'localhost'
120+
self.netloc = None
121+
self.path = '/'
122+
self.port = 443
123+
self.query = None
124+
self.scheme = 'https'
125+
126+
info = UrlInfo()
127+
_result = urlsplit(args._url)
128+
for attr in vars(info).keys():
129+
value = getattr(_result, attr, None)
130+
if value:
131+
setattr(info, attr, value)
132+
133+
if info.scheme == 'http' and not _result.port:
134+
info.port = 80
135+
136+
if info.netloc:
137+
hostname, _ = split_host_and_port(info.netloc)
138+
info.host = hostname # ensure stripping port number
139+
else:
140+
if _result.path:
141+
_path = _result.path.split('/', 1)
142+
hostname, port = split_host_and_port(_path[0])
143+
info.host = hostname
144+
if info.path == _path[0]:
145+
info.path = '/'
146+
elif len(_path) == 2 and _path[1]:
147+
info.path = '/' + _path[1]
148+
if port is not None:
149+
info.port = port
150+
151+
log.debug('url info: %s', vars(info))
152+
args.url = info
153+
154+
155+
def set_request_data(args):
156+
body, headers, params = {}, {}, {}
157+
for i in args.items:
158+
if i.sep == SEP_HEADERS:
159+
headers[i.key] = i.value
160+
elif i.sep == SEP_QUERY:
161+
params[i.key] = i.value
162+
elif i.sep == SEP_DATA:
163+
body[i.key] = i.value
164+
165+
if params:
166+
args.url.path += '?' + urlencode(params)
167+
168+
if body:
169+
content_type = 'application/json; charset=%s' % FILESYSTEM_ENCODING
170+
headers.setdefault('content-type', content_type)
171+
args.body = json.dumps(body)
172+
173+
if args.method is None:
174+
args.method = 'POST' if args.body else 'GET'
175+
176+
args.headers = headers
177+
178+
179+
def parse_argument(argv=None):
180+
parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
181+
parser.set_defaults(body=None, headers={})
182+
make_positional_argument(parser)
183+
make_troubleshooting_argument(parser)
184+
args = parser.parse_args(sys.argv[1:] if argv is None else argv)
185+
186+
if args.debug:
187+
handler = logging.StreamHandler()
188+
handler.setLevel(logging.DEBUG)
189+
log.addHandler(handler)
190+
log.setLevel(logging.DEBUG)
191+
192+
set_url_info(args)
193+
set_request_data(args)
194+
return args
195+
196+
197+
def get_content_type_and_charset(response):
198+
charset = 'utf-8'
199+
content_type = response.getheader('content-type')
200+
if content_type is None:
201+
return 'unknown', charset
202+
203+
content_type = content_type.lower()
204+
type_and_charset = content_type.split(';', 1)
205+
ctype = type_and_charset[0].strip()
206+
if len(type_and_charset) == 2:
207+
charset = type_and_charset[1].strip().split('=')[1]
208+
209+
return ctype, charset
210+
211+
212+
def request(args):
213+
conn = HTTP20Connection(args.url.host, args.url.port)
214+
conn.request(args.method, args.url.path, args.body, args.headers)
215+
response = conn.getresponse()
216+
log.debug('Response Headers:\n%s', pformat(response.getheaders()))
217+
ctype, charset = get_content_type_and_charset(response)
218+
data = response.read().decode(charset)
219+
if 'json' in ctype:
220+
data = pformat(json.loads(data))
221+
return data
222+
223+
224+
def main(argv=None):
225+
args = parse_argument(argv)
226+
log.debug('Commandline Argument: %s', args)
227+
print(request(args))
228+
229+
230+
if __name__ == '__main__': # pragma: no cover
231+
main()

hyper/compat.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def ignore_missing():
3434
else:
3535
ssl = ssl_compat
3636

37-
from urlparse import urlparse
37+
from urllib import urlencode
38+
from urlparse import urlparse, urlsplit
3839

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

5051
elif is_py3:
51-
from urllib.parse import urlparse
52+
from urllib.parse import urlencode, urlparse, urlsplit
5253

5354
def to_byte(char):
5455
return char

setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,9 @@ def resolve_install_requires():
6363
'Programming Language :: Python :: Implementation :: CPython',
6464
],
6565
install_requires=resolve_install_requires(),
66+
entry_points={
67+
'console_scripts': [
68+
'hyper = hyper.cli:main',
69+
],
70+
},
6671
)

0 commit comments

Comments
 (0)