mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 08:08:41 +02:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
c446d756ab | |||
7ca6191902 | |||
ebb271334b | |||
bd9209f77a | |||
7d629b4d94 | |||
a44ef6c443 | |||
f4dde9d9b3 | |||
6d14097844 | |||
8a4f501706 | |||
6774998012 | |||
2195280a70 | |||
f5d5ec22af | |||
b728710760 | |||
715e1b1047 | |||
ca8779d879 | |||
5ff43b659f | |||
b802f2b960 | |||
73d0f9cd56 | |||
00312ead28 | |||
d02ac54130 | |||
81568cf91c | |||
6df9ff67eb | |||
5d3176115a | |||
81798ad537 | |||
dd8faecbf7 | |||
58f74fe14a | |||
84a0d4a35d | |||
d670513c9f | |||
860a851a4b | |||
9634dca7d8 | |||
bb653bf1a9 | |||
94c605fac1 | |||
3442a5d037 | |||
5cd40916fe | |||
ed3a491c81 | |||
d768959084 | |||
f934f4345e | |||
b752b59d92 | |||
553941c98d |
11
.travis.yml
Normal file
11
.travis.yml
Normal file
@ -0,0 +1,11 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
# TODO: Python 3
|
||||
#- 3.2
|
||||
script: python tests.py
|
||||
install:
|
||||
- pip install requests pygments
|
||||
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install argparse; fi"
|
||||
|
50
README.md
50
README.md
@ -1,13 +1,22 @@
|
||||
## HTTPie: cURL for humans
|
||||
|
||||
HTTPie is a CLI frontend for [python-requests](http://python-requests.org) built out of frustration. It provides an `http` command that can be used to easily issue HTTP requests. It is meant to be used by humans to interact with HTTP-based APIs and web servers. The response headers are colorized and the body is syntax-highlighed if its `Content-Type` is known to [Pygments](http://pygments.org/) (unless the output is redirected).
|
||||
[](http://travis-ci.org/jkbr/httpie)
|
||||
|
||||
|
||||
HTTPie is a CLI frontend for [python-requests](http://python-requests.org) built out of frustration. It provides an `http` command that can be used to easily issue HTTP requests. It is meant to be used by humans to interact with HTTP-based APIs and web servers. The response headers are colorized and the body is syntax-highlighted if its `Content-Type` is known to [Pygments](http://pygments.org/) (unless the output is redirected).
|
||||
|
||||

|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
pip install httpie
|
||||
Latest stable version using [pip](http://www.pip-installer.org/en/latest/index.html):
|
||||
|
||||
pip install -U httpie
|
||||
# easy_install httpie
|
||||
|
||||
Master:
|
||||
|
||||
pip install -U https://github.com/jkbr/httpie/tarball/master
|
||||
|
||||
|
||||
### Usage
|
||||
@ -27,6 +36,14 @@ Will issue the following request:
|
||||
|
||||
{"name": "John", "email": "john@example.org"}
|
||||
|
||||
You can pass other types than just strings using the `field:=value` notation. It allows you to set arbitrary JSON to the data fields:
|
||||
|
||||
http PUT httpie.org/pies bool:=true list:=[1,2,3] 'object:={"a": "b", "c": "d"}'
|
||||
|
||||
Produces the following JSON request:
|
||||
|
||||
{"bool": true, "list": [1, 2, 3], "object": {"a": "b", "c": "d"}}
|
||||
|
||||
You can use the `--form` flag to set `Content-Type` and serialize the data as `application/x-www-form-urlencoded`.
|
||||
|
||||
The data to be sent can also be passed via `stdin`:
|
||||
@ -35,33 +52,43 @@ The data to be sent can also be passed via `stdin`:
|
||||
|
||||
Most of the flags mirror the arguments you would use with `requests.request`. See `http -h`:
|
||||
|
||||
$ http -h
|
||||
usage: http [-h] [--json | --form] [--traceback] [--ugly] [--headers | --body]
|
||||
usage: http [-h] [--version] [--json | --form] [--traceback]
|
||||
[--pretty | --ugly] [--headers | --body] [--style STYLE]
|
||||
[--auth AUTH] [--verify VERIFY] [--proxy PROXY]
|
||||
[--allow-redirects] [--file PATH] [--timeout TIMEOUT]
|
||||
method URL [item [item ...]]
|
||||
METHOD URL [items [items ...]]
|
||||
|
||||
HTTPie - cURL for humans.
|
||||
|
||||
positional arguments:
|
||||
method HTTP method to be used for the request (GET, POST,
|
||||
METHOD HTTP method to be used for the request (GET, POST,
|
||||
PUT, DELETE, PATCH, ...).
|
||||
URL Protocol defaults to http:// if the URL does not
|
||||
include it.
|
||||
item HTTP header (key:value) or data field (key=value)
|
||||
items HTTP header (key:value), data field (key=value) or raw
|
||||
JSON field (field:=value).
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--version show program's version number and exit
|
||||
--json, -j Serialize data items as a JSON object and set Content-
|
||||
Type to application/json, if not specified.
|
||||
--form, -f Serialize data items as form values and set Content-
|
||||
Type to application/x-www-form-urlencoded, if not
|
||||
specified.
|
||||
--traceback Print a full exception traceback should one be raised
|
||||
by `requests`.
|
||||
--traceback Print exception traceback should one occur.
|
||||
--pretty, -p 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.
|
||||
--ugly, -u Do not prettify the response.
|
||||
--headers, -t Print only the response headers.
|
||||
--body, -b Print only the response body.
|
||||
--style STYLE, -s STYLE
|
||||
Output coloring style, one of autumn, borland, bw,
|
||||
colorful, default, emacs, friendly, fruity, manni,
|
||||
monokai, murphy, native, pastie, perldoc, solarized,
|
||||
tango, trac, vim, vs. Defaults to solarized.
|
||||
--auth AUTH, -a AUTH username:password
|
||||
--verify VERIFY Set to "yes" to check the host's SSL certificate. You
|
||||
can also pass the path to a CA_BUNDLE file for private
|
||||
@ -75,3 +102,6 @@ Most of the flags mirror the arguments you would use with `requests.request`. Se
|
||||
--timeout TIMEOUT Float describes the timeout of the request (Use
|
||||
socket.setdefaulttimeout() as fallback).
|
||||
|
||||
### Changelog
|
||||
|
||||
* [0.1.6](https://github.com/jkbr/httpie/compare/0.1.4...0.1.6) (2012-03-04)
|
||||
|
@ -3,5 +3,5 @@ HTTPie - cURL for humans.
|
||||
|
||||
"""
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.1.4'
|
||||
__version__ = '0.1.6'
|
||||
__licence__ = 'BSD'
|
||||
|
116
httpie/__main__.py
Normal file
116
httpie/__main__.py
Normal file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
OrderedDict = dict
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from . import cli
|
||||
from . import pretty
|
||||
from . import __version__ as version
|
||||
|
||||
|
||||
DEFAULT_UA = 'HTTPie/%s' % version
|
||||
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
TYPE_JSON = 'application/json; charset=utf-8'
|
||||
|
||||
|
||||
def main(args=None,
|
||||
stdin=sys.stdin,
|
||||
stdin_isatty=sys.stdin.isatty(),
|
||||
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 == 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 = 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.
|
||||
if args.json or (not args.form and data):
|
||||
if stdin_isatty:
|
||||
data = json.dumps(data)
|
||||
if 'Content-Type' not in headers and (data or args.json):
|
||||
headers['Content-Type'] = TYPE_JSON
|
||||
elif 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = TYPE_FORM
|
||||
|
||||
# Fire the request.
|
||||
try:
|
||||
response = requests.request(
|
||||
method=args.method.lower(),
|
||||
url=args.url if '://' in args.url else 'http://%s' % args.url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
verify=True if args.verify == 'yes' else args.verify,
|
||||
timeout=args.timeout,
|
||||
auth=(args.auth.key, args.auth.value) if args.auth else None,
|
||||
proxies=dict((p.key, p.value) for p in args.proxy),
|
||||
files=dict((os.path.basename(f.name), f) for f in args.file),
|
||||
allow_redirects=args.allow_redirects,
|
||||
)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
if args.traceback:
|
||||
raise
|
||||
sys.stderr.write(str(e.message) + '\n')
|
||||
sys.exit(1)
|
||||
|
||||
# Reconstruct the raw response.
|
||||
encoding = response.encoding or 'ISO-8859-1'
|
||||
original = response.raw._original_response
|
||||
status_line, headers, body = (
|
||||
'HTTP/{version} {status} {reason}'.format(
|
||||
version='.'.join(str(original.version)),
|
||||
status=original.status, reason=original.reason,
|
||||
),
|
||||
str(original.msg).decode(encoding),
|
||||
response.content.decode(encoding) if response.content else u''
|
||||
)
|
||||
|
||||
if do_prettify:
|
||||
prettify = pretty.PrettyHttp(args.style)
|
||||
if args.print_headers:
|
||||
status_line = prettify.headers(status_line)
|
||||
headers = prettify.headers(headers)
|
||||
if args.print_body and 'Content-Type' in response.headers:
|
||||
body = prettify.body(body, response.headers['Content-Type'])
|
||||
|
||||
# Output.
|
||||
# TODO: preserve leading/trailing whitespaces in the body.
|
||||
# Some of the Pygments styles add superfluous line breaks.
|
||||
if args.print_headers:
|
||||
stdout.write(status_line.strip())
|
||||
stdout.write('\n')
|
||||
stdout.write(headers.strip().encode('utf-8'))
|
||||
stdout.write('\n\n')
|
||||
if args.print_body:
|
||||
stdout.write(body.strip().encode('utf-8'))
|
||||
stdout.write('\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
217
httpie/cli.py
Normal file
217
httpie/cli.py
Normal file
@ -0,0 +1,217 @@
|
||||
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(),)
|
||||
parser.add_argument('--version', action='version', version=version)
|
||||
|
||||
# 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).
|
||||
''')
|
||||
)
|
183
httpie/httpie.py
183
httpie/httpie.py
@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from collections import namedtuple
|
||||
import requests
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from . import pretty
|
||||
from . import __version__ as version
|
||||
from . import __doc__ as doc
|
||||
|
||||
|
||||
DEFAULT_UA = 'HTTPie/%s' % version
|
||||
SEP_COMMON = ':'
|
||||
SEP_DATA = '='
|
||||
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
TYPE_JSON = 'application/json; charset=utf-8'
|
||||
|
||||
|
||||
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:
|
||||
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`.')
|
||||
parser.add_argument('--ugly', '-u', help='Do not prettify the response.',
|
||||
dest='prettify', action='store_false', default=True)
|
||||
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.')
|
||||
|
||||
|
||||
# ``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',
|
||||
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', metavar='item', nargs='*',
|
||||
type=KeyValueType([SEP_COMMON, SEP_DATA]),
|
||||
help='HTTP header (key:value) or data field (key=value)')
|
||||
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse request headers and data from the command line.
|
||||
headers = CaseInsensitiveDict()
|
||||
headers['User-Agent'] = DEFAULT_UA
|
||||
data = {}
|
||||
for item in args.items:
|
||||
if item.sep == SEP_COMMON:
|
||||
target = headers
|
||||
else:
|
||||
if not sys.stdin.isatty():
|
||||
parser.error('Request body (stdin) and request '
|
||||
'data (key=value) cannot be mixed.')
|
||||
target = data
|
||||
target[item.key] = item.value
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.read()
|
||||
|
||||
# JSON/Form content type.
|
||||
if args.json or (not args.form and data):
|
||||
if sys.stdin.isatty():
|
||||
data = json.dumps(data)
|
||||
if 'Content-Type' not in headers and (data or args.json):
|
||||
headers['Content-Type'] = TYPE_JSON
|
||||
elif 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = TYPE_FORM
|
||||
|
||||
# Fire the request.
|
||||
try:
|
||||
response = requests.request(
|
||||
method=args.method.lower(),
|
||||
url=args.url if '://' in args.url else 'http://%s' % args.url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
verify=True if args.verify == 'yes' else args.verify,
|
||||
timeout=args.timeout,
|
||||
auth=(args.auth.key, args.auth.value) if args.auth else None,
|
||||
proxies=dict((p.key, p.value) for p in args.proxy),
|
||||
files=dict((os.path.basename(f.name), f) for f in args.file),
|
||||
)
|
||||
except (KeyboardInterrupt, SystemExit) as e:
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
if args.traceback:
|
||||
raise
|
||||
sys.stderr.write(str(e.message) + '\n')
|
||||
sys.exit(1)
|
||||
|
||||
# Display the response.
|
||||
encoding = response.encoding or 'ISO-8859-1'
|
||||
original = response.raw._original_response
|
||||
status_line, headers, body = (
|
||||
u'HTTP/{version} {status} {reason}'.format(
|
||||
version='.'.join(str(original.version)),
|
||||
status=original.status, reason=original.reason,
|
||||
),
|
||||
str(original.msg).decode(encoding),
|
||||
response.content.decode(encoding) if response.content else u''
|
||||
)
|
||||
|
||||
if args.prettify and sys.stdout.isatty():
|
||||
if args.print_headers:
|
||||
status_line = pretty.prettify_http(status_line).strip()
|
||||
headers = pretty.prettify_http(headers)
|
||||
if args.print_body:
|
||||
body = pretty.prettify_body(body,
|
||||
response.headers['content-type'])
|
||||
|
||||
if args.print_headers:
|
||||
print status_line
|
||||
print headers
|
||||
if args.print_body:
|
||||
print body
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,14 +1,22 @@
|
||||
import os
|
||||
import json
|
||||
from functools import partial
|
||||
import pygments
|
||||
from pygments import token
|
||||
from pygments.util import ClassNotFound
|
||||
from pygments.lexers import get_lexer_for_mimetype
|
||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
from pygments.lexer import RegexLexer, bygroups
|
||||
from pygments import token
|
||||
from pygments.styles import get_style_by_name, STYLE_MAP
|
||||
from . import solarized
|
||||
|
||||
|
||||
DEFAULT_STYLE = 'solarized'
|
||||
AVAILABLE_STYLES = [DEFAULT_STYLE] + STYLE_MAP.keys()
|
||||
TYPE_JS = 'application/javascript'
|
||||
FORMATTER = (Terminal256Formatter
|
||||
if os.environ.get('TERM') == 'xterm-256color'
|
||||
else TerminalFormatter)
|
||||
|
||||
|
||||
class HTTPLexer(RegexLexer):
|
||||
@ -24,32 +32,31 @@ class HTTPLexer(RegexLexer):
|
||||
]}
|
||||
|
||||
|
||||
highlight = partial(pygments.highlight,
|
||||
formatter=Terminal256Formatter(
|
||||
style=solarized.SolarizedStyle))
|
||||
highlight_http = partial(highlight, lexer=HTTPLexer())
|
||||
class PrettyHttp(object):
|
||||
|
||||
def __init__(self, style_name):
|
||||
if style_name == 'solarized':
|
||||
style = solarized.SolarizedStyle
|
||||
else:
|
||||
style = get_style_by_name(style_name)
|
||||
self.formatter = FORMATTER(style=style)
|
||||
|
||||
def prettify_http(headers):
|
||||
return highlight_http(headers)
|
||||
def headers(self, content):
|
||||
return pygments.highlight(content, HTTPLexer(), self.formatter)
|
||||
|
||||
|
||||
def prettify_body(content, content_type):
|
||||
content_type = content_type.split(';')[0]
|
||||
if 'json' in content_type:
|
||||
content_type = TYPE_JS
|
||||
def body(self, content, content_type):
|
||||
content_type = content_type.split(';')[0]
|
||||
if 'json' in content_type:
|
||||
content_type = TYPE_JS
|
||||
try:
|
||||
# Indent JSON
|
||||
content = json.dumps(json.loads(content),
|
||||
sort_keys=True, indent=4)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# Indent JSON
|
||||
content = json.dumps(json.loads(content),
|
||||
sort_keys=True, indent=4)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
lexer = get_lexer_for_mimetype(content_type)
|
||||
content = highlight(code=content, lexer=lexer)
|
||||
if content:
|
||||
content = content[:-1]
|
||||
except Exception:
|
||||
pass
|
||||
return content
|
||||
|
||||
lexer = get_lexer_for_mimetype(content_type)
|
||||
except ClassNotFound:
|
||||
return content
|
||||
content = pygments.highlight(content, lexer, self.formatter)
|
||||
return content
|
||||
|
54
setup.py
54
setup.py
@ -1,12 +1,58 @@
|
||||
import os
|
||||
import sys
|
||||
from setuptools import setup
|
||||
import httpie
|
||||
|
||||
|
||||
setup(name='httpie',version=httpie.__version__,
|
||||
if sys.argv[-1] == 'test':
|
||||
os.system('python tests.py')
|
||||
sys.exit()
|
||||
|
||||
|
||||
requirements = ['requests>=0.10.4', 'Pygments>=1.4']
|
||||
if sys.version_info < (2, 7):
|
||||
requirements.append('argparse>=1.2.1')
|
||||
|
||||
|
||||
try:
|
||||
long_description = open('README.md').read()
|
||||
except IOError:
|
||||
long_description = ''
|
||||
|
||||
|
||||
setup(
|
||||
name='httpie',version=httpie.__version__,
|
||||
description=httpie.__doc__.strip(),
|
||||
url='https://github.com/jkbr/httpie',
|
||||
long_description=long_description,
|
||||
url='http://httpie.org/',
|
||||
download_url='https://github.com/jkbr/httpie',
|
||||
author=httpie.__author__,
|
||||
author_email='jakub@roztocil.name',
|
||||
license=httpie.__licence__,
|
||||
packages=['httpie'],
|
||||
entry_points={'console_scripts': ['http = httpie.httpie:main']},
|
||||
install_requires=['requests>=0.10.4', 'Pygments>=1.4'])
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'http = httpie.__main__:main',
|
||||
],
|
||||
},
|
||||
install_requires=requirements,
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
# TODO: Python 3
|
||||
# 'Programming Language :: Python :: 3.1'
|
||||
# 'Programming Language :: Python :: 3.2'
|
||||
# 'Programming Language :: Python :: 3.3'
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Software Development',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: Terminals',
|
||||
'Topic :: Text Processing',
|
||||
],
|
||||
)
|
||||
|
115
tests.py
Normal file
115
tests.py
Normal file
@ -0,0 +1,115 @@
|
||||
import sys
|
||||
import unittest
|
||||
import argparse
|
||||
from StringIO import StringIO
|
||||
from httpie import __main__
|
||||
from httpie import cli
|
||||
|
||||
|
||||
TERMINAL_COLOR_CHECK = '\x1b['
|
||||
|
||||
|
||||
def http(*args, **kwargs):
|
||||
http_kwargs = {
|
||||
'stdin_isatty': True,
|
||||
'stdout_isatty': False
|
||||
}
|
||||
http_kwargs.update(kwargs)
|
||||
stdout = http_kwargs.setdefault('stdout', StringIO())
|
||||
__main__.main(args=args, **http_kwargs)
|
||||
return stdout.getvalue()
|
||||
|
||||
|
||||
class BaseTest(unittest.TestCase):
|
||||
|
||||
if sys.version < (2, 7):
|
||||
def assertIn(self, member, container, msg=None):
|
||||
self.assert_(member in container, msg)
|
||||
|
||||
def assertNotIn(self, member, container, msg=None):
|
||||
self.assert_(member not in container, msg)
|
||||
|
||||
def assertDictEqual(self, d1, d2, msg=None):
|
||||
self.assertEqual(set(d1.keys()), set(d2.keys()), msg)
|
||||
self.assertEqual(sorted(d1.values()), sorted(d2.values()), msg)
|
||||
|
||||
|
||||
class TestItemParsing(BaseTest):
|
||||
|
||||
def setUp(self):
|
||||
self.kv = cli.KeyValueType(
|
||||
cli.SEP_HEADERS,
|
||||
cli.SEP_DATA,
|
||||
cli.SEP_DATA_RAW_JSON
|
||||
)
|
||||
|
||||
def test_invalid_items(self):
|
||||
items = ['no-separator']
|
||||
for item in items:
|
||||
self.assertRaises(argparse.ArgumentTypeError,
|
||||
lambda: self.kv(item))
|
||||
|
||||
def test_valid_items(self):
|
||||
headers, data = cli.parse_items([
|
||||
self.kv('string=value'),
|
||||
self.kv('header:value'),
|
||||
self.kv('list:=["a", 1, {}, false]'),
|
||||
self.kv('obj:={"a": "b"}'),
|
||||
self.kv('eh:'),
|
||||
self.kv('ed='),
|
||||
self.kv('bool:=true'),
|
||||
])
|
||||
self.assertDictEqual(headers, {
|
||||
'header': 'value',
|
||||
'eh': ''
|
||||
})
|
||||
self.assertDictEqual(data, {
|
||||
"ed": "",
|
||||
"string": "value",
|
||||
"bool": True,
|
||||
"list": ["a", 1, {}, False],
|
||||
"obj": {"a": "b"}
|
||||
})
|
||||
|
||||
|
||||
class TestHTTPie(BaseTest):
|
||||
|
||||
def test_get(self):
|
||||
http('GET', 'http://httpbin.org/get')
|
||||
|
||||
def test_json(self):
|
||||
response = http('POST', 'http://httpbin.org/post', 'foo=bar')
|
||||
self.assertIn('"foo": "bar"', response)
|
||||
|
||||
def test_form(self):
|
||||
response = http('POST', '--form', 'http://httpbin.org/post', 'foo=bar')
|
||||
self.assertIn('"foo": "bar"', response)
|
||||
|
||||
def test_headers(self):
|
||||
response = http('GET', 'http://httpbin.org/headers', 'Foo:bar')
|
||||
self.assertIn('"User-Agent": "HTTPie', response)
|
||||
self.assertIn('"Foo": "bar"', response)
|
||||
|
||||
|
||||
class TestPrettyFlag(BaseTest):
|
||||
"""Test the --pretty / --ugly flag handling."""
|
||||
|
||||
def test_pretty_enabled_by_default(self):
|
||||
r = http('GET', 'http://httpbin.org/get', stdout_isatty=True)
|
||||
self.assertIn(TERMINAL_COLOR_CHECK, r)
|
||||
|
||||
def test_pretty_enabled_by_default_unless_stdin_redirected(self):
|
||||
r = http('GET', 'http://httpbin.org/get', stdout_isatty=False)
|
||||
self.assertNotIn(TERMINAL_COLOR_CHECK, r)
|
||||
|
||||
def test_force_pretty(self):
|
||||
r = http('GET', '--pretty', 'http://httpbin.org/get', stdout_isatty=False)
|
||||
self.assertIn(TERMINAL_COLOR_CHECK, r)
|
||||
|
||||
def test_force_ugly(self):
|
||||
r = http('GET', '--ugly', 'http://httpbin.org/get', stdout_isatty=True)
|
||||
self.assertNotIn(TERMINAL_COLOR_CHECK, r)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Reference in New Issue
Block a user