mirror of
https://github.com/httpie/cli.git
synced 2025-08-15 07:42:57 +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: 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
|
### 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
|
### Usage
|
||||||
@ -27,6 +36,14 @@ Will issue the following request:
|
|||||||
|
|
||||||
{"name": "John", "email": "john@example.org"}
|
{"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`.
|
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`:
|
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`:
|
Most of the flags mirror the arguments you would use with `requests.request`. See `http -h`:
|
||||||
|
|
||||||
$ http -h
|
usage: http [-h] [--version] [--json | --form] [--traceback]
|
||||||
usage: http [-h] [--json | --form] [--traceback] [--ugly] [--headers | --body]
|
[--pretty | --ugly] [--headers | --body] [--style STYLE]
|
||||||
[--auth AUTH] [--verify VERIFY] [--proxy PROXY]
|
[--auth AUTH] [--verify VERIFY] [--proxy PROXY]
|
||||||
[--allow-redirects] [--file PATH] [--timeout TIMEOUT]
|
[--allow-redirects] [--file PATH] [--timeout TIMEOUT]
|
||||||
method URL [item [item ...]]
|
METHOD URL [items [items ...]]
|
||||||
|
|
||||||
HTTPie - cURL for humans.
|
HTTPie - cURL for humans.
|
||||||
|
|
||||||
positional arguments:
|
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, ...).
|
PUT, DELETE, PATCH, ...).
|
||||||
URL Protocol defaults to http:// if the URL does not
|
URL Protocol defaults to http:// if the URL does not
|
||||||
include it.
|
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:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-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-
|
--json, -j Serialize data items as a JSON object and set Content-
|
||||||
Type to application/json, if not specified.
|
Type to application/json, if not specified.
|
||||||
--form, -f Serialize data items as form values and set Content-
|
--form, -f Serialize data items as form values and set Content-
|
||||||
Type to application/x-www-form-urlencoded, if not
|
Type to application/x-www-form-urlencoded, if not
|
||||||
specified.
|
specified.
|
||||||
--traceback Print a full exception traceback should one be raised
|
--traceback Print exception traceback should one occur.
|
||||||
by `requests`.
|
--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.
|
--ugly, -u Do not prettify the response.
|
||||||
--headers, -t Print only the response headers.
|
--headers, -t Print only the response headers.
|
||||||
--body, -b Print only the response body.
|
--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
|
--auth AUTH, -a AUTH username:password
|
||||||
--verify VERIFY Set to "yes" to check the host's SSL certificate. You
|
--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
|
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
|
--timeout TIMEOUT Float describes the timeout of the request (Use
|
||||||
socket.setdefaulttimeout() as fallback).
|
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'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.1.4'
|
__version__ = '0.1.6'
|
||||||
__licence__ = 'BSD'
|
__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
|
import json
|
||||||
from functools import partial
|
|
||||||
import pygments
|
import pygments
|
||||||
|
from pygments import token
|
||||||
|
from pygments.util import ClassNotFound
|
||||||
from pygments.lexers import get_lexer_for_mimetype
|
from pygments.lexers import get_lexer_for_mimetype
|
||||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
from pygments.formatters.terminal256 import Terminal256Formatter
|
||||||
|
from pygments.formatters.terminal import TerminalFormatter
|
||||||
from pygments.lexer import RegexLexer, bygroups
|
from pygments.lexer import RegexLexer, bygroups
|
||||||
from pygments import token
|
from pygments.styles import get_style_by_name, STYLE_MAP
|
||||||
from . import solarized
|
from . import solarized
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_STYLE = 'solarized'
|
||||||
|
AVAILABLE_STYLES = [DEFAULT_STYLE] + STYLE_MAP.keys()
|
||||||
TYPE_JS = 'application/javascript'
|
TYPE_JS = 'application/javascript'
|
||||||
|
FORMATTER = (Terminal256Formatter
|
||||||
|
if os.environ.get('TERM') == 'xterm-256color'
|
||||||
|
else TerminalFormatter)
|
||||||
|
|
||||||
|
|
||||||
class HTTPLexer(RegexLexer):
|
class HTTPLexer(RegexLexer):
|
||||||
@ -24,17 +32,19 @@ class HTTPLexer(RegexLexer):
|
|||||||
]}
|
]}
|
||||||
|
|
||||||
|
|
||||||
highlight = partial(pygments.highlight,
|
class PrettyHttp(object):
|
||||||
formatter=Terminal256Formatter(
|
|
||||||
style=solarized.SolarizedStyle))
|
|
||||||
highlight_http = partial(highlight, lexer=HTTPLexer())
|
|
||||||
|
|
||||||
|
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):
|
def headers(self, content):
|
||||||
return highlight_http(headers)
|
return pygments.highlight(content, HTTPLexer(), self.formatter)
|
||||||
|
|
||||||
|
def body(self, content, content_type):
|
||||||
def prettify_body(content, content_type):
|
|
||||||
content_type = content_type.split(';')[0]
|
content_type = content_type.split(';')[0]
|
||||||
if 'json' in content_type:
|
if 'json' in content_type:
|
||||||
content_type = TYPE_JS
|
content_type = TYPE_JS
|
||||||
@ -46,10 +56,7 @@ def prettify_body(content, content_type):
|
|||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
lexer = get_lexer_for_mimetype(content_type)
|
lexer = get_lexer_for_mimetype(content_type)
|
||||||
content = highlight(code=content, lexer=lexer)
|
except ClassNotFound:
|
||||||
if content:
|
return content
|
||||||
content = content[:-1]
|
content = pygments.highlight(content, lexer, self.formatter)
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
54
setup.py
54
setup.py
@ -1,12 +1,58 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
import httpie
|
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(),
|
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=httpie.__author__,
|
||||||
|
author_email='jakub@roztocil.name',
|
||||||
license=httpie.__licence__,
|
license=httpie.__licence__,
|
||||||
packages=['httpie'],
|
packages=['httpie'],
|
||||||
entry_points={'console_scripts': ['http = httpie.httpie:main']},
|
entry_points={
|
||||||
install_requires=['requests>=0.10.4', 'Pygments>=1.4'])
|
'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