Compare commits

..

38 Commits
0.2.1 ... 0.2.4

Author SHA1 Message Date
deeb7cbbac 0.2.4 (bad upload of 0.2.3 to pypi). 2012-07-17 00:44:25 +02:00
12f2fb4a92 Merge branch 'master' of github.com:jkbr/httpie 2012-07-17 00:38:41 +02:00
489bd64295 0.2.4dev 2012-07-17 00:37:53 +02:00
9b8cb42efd 0.2.3 2012-07-17 00:37:13 +02:00
2036337a53 Merge pull request #69 from jokull/master
Prettify vendor+json and vendor+xml Content-Type responses
2012-07-16 15:27:50 -07:00
5ca8bec9ff Add a note on pretty JSON and unicode to changelog
Closes #52
Closes #67
2012-07-17 00:22:39 +02:00
df79792fd9 Added test case to verify unicode output 2012-07-17 00:09:01 +02:00
5a82c79fdf Now non-ascii symbols displayed correctly in the output (not as escape sequences). 2012-07-17 00:08:52 +02:00
05b321d38f Better wording. 2012-07-17 00:06:13 +02:00
681b652bf9 Allow stdin data with password prompt; added tests
Closes #70
2012-07-16 23:41:27 +02:00
85b3a016eb Update README with new --auth behavior. 2012-07-16 04:50:25 -04:00
929ead437a Have --auth prompt for password if omitted. 2012-07-16 04:40:36 -04:00
36de166b28 Simplify vendor extension content-types since they are most likely lexable 2012-07-14 14:27:11 +00:00
7bc2de2f9d Merge pull request #68 from cemaleker/master
Added omitted query string data to request headers.
2012-07-13 17:53:11 -07:00
cb7ead04e2 Added omitted query string data to request headers. 2012-07-14 03:37:24 +03:00
cd2ca41f48 Merge pull request #65 from simono/patch-1
Update README.rst and add links to Ubuntu and Debian Packages.
2012-07-11 06:35:28 -07:00
c71de95505 Update README.rst and add links to Ubuntu and Debian Packages. 2012-07-11 16:32:00 +03:00
6ab03b21b4 Fixed Content-Type for requests with no data.
Closes #62.
2012-07-04 01:39:21 +02:00
50196be0f2 Added support for request payload from a filepath
Content-Type is detected from the filename.

Closes #57.
2012-06-29 00:45:31 +02:00
41d640920c Added more examples. 2012-06-25 14:50:49 +02:00
3179631603 0.2.3dev 2012-06-24 16:45:01 +02:00
2f7921091c 0.2.2 2012-06-24 16:43:03 +02:00
180313d80c Impreved tests. 2012-06-24 04:20:45 +02:00
926d3f5caf Tests, docs, clean-up.
Closes #54.
2012-06-24 03:45:21 +02:00
4613d947a8 Default to POST also when stdin redirected.
+clean up
2012-06-24 01:25:30 +02:00
5a47f00bac Replaced mock.Mock with argparse.Namespace to reduce deps. 2012-06-23 23:54:59 +02:00
0e1affbbc4 Issue #54 Method suggestion proposal 2012-06-17 22:15:07 +04:00
d920f20847 Issue #54 Method suggestion proposal 2012-06-17 22:11:26 +04:00
bca36f0464 Issue #54 Method suggestion proposal 2012-06-17 21:46:56 +04:00
78fff98712 Issue #54 Method suggestion proposal 2012-06-16 20:08:31 +04:00
e06c448a75 README improvements. 2012-06-15 17:32:45 +02:00
9cdbd6b0ec Added a Contribute section to README. 2012-06-15 17:13:40 +02:00
cbc6d02127 Fixed --verbose --form.
Closes #53
2012-06-15 16:47:55 +02:00
284a75fa2f Merge pull request #51 from msabramo/testing
Added support for tox (http://tox.testrun.org/)
2012-06-13 07:48:09 -07:00
b3ea273a21 Add "pypy" to .travis.yml 2012-06-13 07:36:51 -07:00
0d129d5f69 Add tox.ini for tox (http://tox.testrun.org/) 2012-06-13 07:18:12 -07:00
1388206f1a Fix path to tests.py in setup.py to make python setup.py test work 2012-06-13 07:18:11 -07:00
28dbe9f76c Bump version to 0.2.2dev. 2012-06-13 16:02:30 +02:00
13 changed files with 681 additions and 155 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ dist
httpie.egg-info
build
*.pyc
.tox

View File

@ -2,10 +2,9 @@ language: python
python:
- 2.6
- 2.7
- pypy
- 3.1
- 3.2
script: python tests/tests.py
script: python setup.py test
install:
- pip install requests pygments
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]] || [[ $TRAVIS_PYTHON_VERSION == '3.1' ]]; then pip install argparse; fi"
- pip install . --use-mirrors

View File

@ -29,16 +29,19 @@ Or, you can install the **development version** directly from GitHub:
pip install -U https://github.com/jkbr/httpie/tarball/master
There are packages available for `Ubuntu <http://packages.ubuntu.com/quantal/httpie>`_ and `Debian <http://packages.debian.org/wheezy/httpie>`_.
Usage
-----
Hello world::
http GET httpie.org
http httpie.org
Synopsis::
http [flags] METHOD URL [items]
http [flags] [METHOD] URL [items]
There are four types of key-value pair items available:
@ -72,7 +75,7 @@ The following request is issued::
{"name": "John", "email": "john@example.org", "age": 29}
It can easily be changed to a 'form' request using the ``-f`` (or ``--form``) flag, which produces::
It can easily be changed to a **form** request using the ``-f`` (or ``--form``) flag, which produces::
PATCH /person/1 HTTP/1.1
User-Agent: HTTPie/0.1
@ -81,7 +84,7 @@ It can easily be changed to a 'form' request using the ``-f`` (or ``--form``) fl
age=29&name=John&email=john%40example.org
It is also possible to send ``multipart/form-data`` requests, i.e., to simulate a file upload form submission. It is done using the ``--form`` / ``-f`` flag and passing one or more file fields::
It is also possible to send ``multipart/form-data`` requests, i.e., to simulate a **file upload form** submission. It is done using the ``--form`` / ``-f`` flag and passing one or more file fields::
http -f POST example.com/jobs name=John cv@~/Documents/cv.pdf
@ -92,50 +95,68 @@ The above will send the same request as if the following HTML form were submitte
<input type="file" name="cv" />
</form>
A whole request body can be passed in via ``stdin`` instead::
A whole request body can be passed in via **``stdin``** instead, in which case it will be used with no further processing::
echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123
# Or:
http POST example.com/person/1 X-API-Token:123 < person.json
That can be used for **piping services together**. The following example ``GET``s JSON data from the Github API and ``POST``s it to httpbin.org::
http -b GET https://api.github.com/repos/jkbr/httpie | http POST httpbin.org/post
The above can be further simplified by omitting ``GET`` and ``POST`` because they are both default here. The first command has no request data, whereas the second one does via ``stdin``::
http -b https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
An alternative to ``stdin`` is to pass a file name whose content will be used as the request body. It has the advantage that the ``Content-Type`` header will automatically be set to the appropriate value based on the filename extension (using the ``mimetypes`` module). Therefore, the following will request will send the verbatim contents of the file with ``Content-Type: application/xml``::
http PUT httpbin.org/put @/data/file.xml
Flags
^^^^^
Most of the flags mirror the arguments understood by ``requests.request``. See ``http -h`` for more details::
$ http --help
usage: http [-h] [--version] [--json | --form] [--traceback]
[--pretty | --ugly]
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
[--style STYLE] [--auth AUTH] [--auth-type {basic,digest}]
[--verify VERIFY] [--proxy PROXY] [--allow-redirects]
[--timeout TIMEOUT]
METHOD URL [ITEM [ITEM ...]]
[--pretty | --ugly]
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
[--style STYLE] [--auth AUTH] [--auth-type {basic,digest}]
[--verify VERIFY] [--proxy PROXY] [--allow-redirects]
[--timeout TIMEOUT]
[METHOD] URL [ITEM [ITEM ...]]
HTTPie - cURL for humans. <http://httpie.org>
positional arguments:
METHOD The HTTP method to be used for the request (GET, POST,
PUT, DELETE, PATCH, ...).
PUT, DELETE, PATCH, ...). If this argument is omitted,
then HTTPie will guess the HTTP method. If there is
some data to be sent, then it will be POST, otherwise
GET.
URL The protocol defaults to http:// if the URL does not
include one.
ITEM A key-value pair whose type is defined by the
separator used. It can be an HTTP header
(header:value), a data field to be used in the request
body (field_name=value), a raw JSON data field
(field_name:=value) or a file field
(field_name:=value), or a file field
(field_name@/path/to/file). You can use a backslash to
escape a colliding separator in the field name.
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
--json, -j (default) Data items are serialized as a JSON object.
The Content-Type and Accept headers are set to
application/json (if not set via the command line).
--form, -f Data items are serialized as form fields. The Content-
Type is set to application/x-www-form-urlencoded (if
not specifid). The presence of any file fields results
into a multipart/form-data request.
--json, -j (default) Data items from the command line are
serialized as a JSON object. The Content-Type and
Accept headers are set to application/json (if not
specified).
--form, -f Data items from the command line are serialized as
form fields. The Content-Type is set to application/x
-www-form-urlencoded (if not specified). The presence
of any file fields results into a multipart/form-data
request.
--traceback Print exception traceback should one occur.
--pretty If stdout is a terminal, the response is prettified by
default (colorized and indented if it is JSON). This
@ -156,13 +177,14 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
--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. For this
option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color"
or similar (e.g., via `export TERM=xterm-256color' in
your ~/.bashrc).
--auth AUTH, -a AUTH username:password
monokai, murphy, native, pastie, perldoc, rrt,
solarized, tango, trac, vim, vs. Defaults to
solarized. For this option to work properly, please
make sure that the $TERM environment variable is set
to "xterm-256color" or similar (e.g., via `export TERM
=xterm-256color' in your ~/.bashrc).
--auth AUTH, -a AUTH username:password. If the password is omitted (-a
username), HTTPie will prompt for it.
--auth-type {basic,digest}
The authentication mechanism to be used. Defaults to
"basic".
@ -179,21 +201,44 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
socket.setdefaulttimeout() as fallback).
Contributors
------------
Contribute
-----------
`View contributors on GitHub <https://github.com/jkbr/httpie/contributors>`_.
If you have found a bug or have a feature request, the `issue tracker <https://github.com/jkbr/httpie/issues?state=open>`_ is the place to start a discussion about it.
To contribute code or documentation, please first browse the existing issues to see if the feature/bug has previously been discussed. Then fork `the repository <https://github.com/jkbr/httpie>`_, make changes in your develop branch and submit a pull request. Note: Pull requests with tests and documentation are 53.6% more awesome :)
Before a pull requests is submitted, it's a good idea to run the existing suite of tests::
python setup.py test
`Tox <http://tox.testrun.org/>`_ can used to conveniently run tests in all of the `supported Python environments <https://github.com/jkbr/httpie/blob/master/tox.ini>`_::
# Install tox
pip install tox
# Run tests
tox
Changelog
---------
* `New in development version <https://github.com/jkbr/httpie/compare/0.2.1...master>`_
* 0.2.1 (2012-06-13)
* `0.2.4 <https://github.com/jkbr/httpie/compare/0.2.2...0.2.4>`_ (2012-06-24)
* Unicode characters in prettified JSON now don't get escaped to improve readability.
* --auth now prompts for a password if only a username provided.
* Added support for request payloads from a file path with automatic ``Content-Type`` (``http URL @/path``).
* Fixed missing query string when displaing the request headers via ``--verbose``.
* Fixed Content-Type for requests with no data.
* `0.2.2 <https://github.com/jkbr/httpie/compare/0.2.1...0.2.2>`_ (2012-06-24)
* The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data).
* Fixed --verbose --form.
* Added support for `Tox <http://tox.testrun.org/>`_.
* `0.2.1 <https://github.com/jkbr/httpie/compare/0.2.0...0.2.1>`_ (2012-06-13)
* Added compatibility with ``requests-0.12.1``.
* Dropped custom JSON and HTTP lexers in favor of the ones newly included in ``pygments-1.5``.
* `Complete changelog <https://github.com/jkbr/httpie/compare/0.2.0...0.2.1>`_
* 0.2.0 (2012-04-25)
* `0.2.0 <https://github.com/jkbr/httpie/compare/0.1.6...0.2.0>`_ (2012-04-25)
* Added Python 3 support.
* Added the ability to print the HTTP request as well as the response (see ``--print`` and ``--verbose``).
* Added support for Digest authentication.
@ -201,5 +246,4 @@ Changelog
* Improved syntax highlighting for JSON.
* Added support for field name escaping.
* Many bug fixes.
* `Complete changelog <https://github.com/jkbr/httpie/compare/0.1.6...0.2.0>`_
* `0.1.6 <https://github.com/jkbr/httpie/compare/0.1.4...0.1.6>`_ (2012-03-04)

View File

@ -3,5 +3,5 @@ HTTPie - cURL for humans.
"""
__author__ = 'Jakub Roztocil'
__version__ = '0.2.1'
__version__ = '0.2.4'
__licence__ = 'BSD'

View File

@ -16,29 +16,29 @@ TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
TYPE_JSON = 'application/json; charset=utf-8'
def _get_response(parser, args, stdin, stdin_isatty):
def _get_response(args):
if not stdin_isatty:
if args.data:
parser.error('Request body (stdin) and request '
'data (key=value) cannot be mixed.')
args.data = stdin.read()
if args.json or (not args.form and args.data):
auto_json = args.data and not args.form
if args.json or auto_json:
# JSON
if not args.files and (
'Content-Type' not in args.headers
and (args.data or args.json)):
args.headers['Content-Type'] = TYPE_JSON
if stdin_isatty:
# Serialize the parsed data.
args.data = json.dumps(args.data)
if 'Content-Type' not in args.headers:
args.headers['Content-Type'] = TYPE_JSON
if 'Accept' not in args.headers:
# Default Accept to JSON as well.
args.headers['Accept'] = 'application/json'
elif not args.files and 'Content-Type' not in args.headers:
if isinstance(args.data, dict):
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
elif args.form:
# Form
args.headers['Content-Type'] = TYPE_FORM
if not args.files and 'Content-Type' not in args.headers:
# If sending files, `requests` will set
# the `Content-Type` for us.
args.headers['Content-Type'] = TYPE_FORM
# Fire the request.
try:
@ -114,8 +114,12 @@ 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:])
response = _get_response(parser, args, stdin, stdin_isatty)
args = parser.parse_args(
args=args if args is not None else sys.argv[1:],
stdin=stdin,
stdin_isatty=stdin_isatty
)
response = _get_response(args)
output = _get_output(args, stdout_isatty, response)
output_bytes = output.encode('utf8')
f = (stdout.buffer if hasattr(stdout, 'buffer') else stdout)

View File

@ -14,7 +14,7 @@ def _(text):
desc = '%s <http://httpie.org>'
parser = cliparse.HTTPieArgumentParser(description=desc % __doc__.strip(),)
parser = cliparse.Parser(description=desc % __doc__.strip(),)
parser.add_argument('--version', action='version', version=__version__)
@ -25,16 +25,16 @@ group_type = parser.add_mutually_exclusive_group(required=False)
group_type.add_argument(
'--json', '-j', action='store_true',
help=_('''
(default) Data items are serialized as a JSON object.
(default) Data items from the command line are serialized as a JSON object.
The Content-Type and Accept headers
are set to application/json (if not set via the command line).
are set to application/json (if not specified).
''')
)
group_type.add_argument(
'--form', '-f', action='store_true',
help=_('''
Data items are serialized as form fields.
The Content-Type is set to application/x-www-form-urlencoded (if not specifid).
Data items from the command line are serialized as form fields.
The Content-Type is set to application/x-www-form-urlencoded (if not specified).
The presence of any file fields results into a multipart/form-data request.
''')
)
@ -123,8 +123,11 @@ parser.add_argument(
# ``requests.request`` keyword arguments.
parser.add_argument(
'--auth', '-a', help='username:password',
type=cliparse.KeyValueType(cliparse.SEP_COMMON)
'--auth', '-a', type=cliparse.AuthCredentialsType(cliparse.SEP_COMMON),
help=_('''
username:password.
If only the username is provided (-a username), HTTPie will prompt for the password.
'''),
)
parser.add_argument(
@ -171,9 +174,13 @@ parser.add_argument(
parser.add_argument(
'method', metavar='METHOD',
nargs='?',
default=None,
help=_('''
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...).
If this argument is omitted, then HTTPie will guess the HTTP method.
If there is some data to be sent, then it will be POST, otherwise GET.
''')
)
parser.add_argument(
@ -196,7 +203,7 @@ parser.add_argument(
A key-value pair whose type is defined by the separator used. It can be an
HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value)
a raw JSON data field (field_name:=value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding separator in the field name.
''')

View File

@ -3,11 +3,12 @@ CLI argument parsing logic.
"""
import os
import sys
import re
import json
import argparse
from collections import namedtuple
import mimetypes
import getpass
try:
from collections import OrderedDict
@ -24,6 +25,11 @@ SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
DATA_ITEM_SEPARATORS = [
SEP_DATA,
SEP_DATA_RAW_JSON,
SEP_FILES
]
OUT_REQ_HEADERS = 'H'
@ -41,16 +47,80 @@ PRETTIFY_STDOUT_TTY_ONLY = object()
DEFAULT_UA = 'HTTPie/%s' % __version__
class HTTPieArgumentParser(argparse.ArgumentParser):
class Parser(argparse.ArgumentParser):
def parse_args(self, args=None, namespace=None,
stdin=sys.stdin,
stdin_isatty=sys.stdin.isatty()):
args = super(Parser, self).parse_args(args, namespace)
def parse_args(self, args=None, namespace=None):
args = super(HTTPieArgumentParser, self).parse_args(args, namespace)
self._validate_output_options(args)
self._validate_auth_options(args)
self._guess_method(args, stdin_isatty)
self._parse_items(args)
if not stdin_isatty:
self._body_from_file(args, stdin)
if args.auth and not args.auth.has_password():
# stdin has already been read (if not a tty) so
# it's save to prompt now.
args.auth.prompt_password()
return args
def _body_from_file(self, args, f):
if args.data:
self.error('Request body (from stdin or a file) and request '
'data (key=value) cannot be mixed.')
args.data = f.read()
def _guess_method(self, args, stdin_isatty=sys.stdin.isatty()):
"""
Set `args.method`, if not specified, to either POST or GET
based on whether the request has data or not.
"""
if args.method is None:
# Invoked as `http URL'.
assert not args.items
if not stdin_isatty:
args.method = 'POST'
else:
args.method = 'GET'
# FIXME: False positive, e.g., "localhost" matches but is a valid URL.
elif not re.match('^[a-zA-Z]+$', args.method):
# Invoked as `http URL item+':
# - The URL is now in `args.method`.
# - The first item is now in `args.url`.
#
# So we need to:
# - Guess the HTTP method.
# - Set `args.url` correctly.
# - Parse the first item and move it to `args.items[0]`.
item = KeyValueType(
SEP_COMMON,
SEP_DATA,
SEP_DATA_RAW_JSON,
SEP_FILES).__call__(args.url)
args.url = args.method
args.items.insert(0, item)
has_data = not stdin_isatty or any(
item.sep in DATA_ITEM_SEPARATORS for item in args.items)
if has_data:
args.method = 'POST'
else:
args.method = 'GET'
def _parse_items(self, args):
"""
Parse `args.items` into `args.headers`, `args.data` and `args.files`.
"""
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = OrderedDict()
@ -64,12 +134,25 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.error(e.message)
if args.files and not args.form:
# We could just switch to --form automatically here,
# but I think it's better to make it explicit.
self.error(
' You need to set the --form / -f flag to'
' to issue a multipart request. File fields: %s'
% ','.join(args.files.keys()))
# `http url @/path/to/file`
# It's not --form so the file contents will be used as the
# body of the requests. Also, we try to detect the appropriate
# Content-Type.
if len(args.files) > 1:
self.error(
'Only one file can be specified unless'
' --form is used. File fields: %s'
% ','.join(args.files.keys()))
f = list(args.files.values())[0]
self._body_from_file(args, f)
args.files = {}
if 'Content-Type' not in args.headers:
mime, encoding = mimetypes.guess_type(f.name, strict=False)
if mime:
content_type = mime
if encoding:
content_type = '%s; charset=%s' % (mime, encoding)
args.headers['Content-Type'] = content_type
def _validate_output_options(self, args):
unknown_output_options = set(args.output_options) - set(OUTPUT_OPTIONS)
@ -85,12 +168,24 @@ class ParseError(Exception):
pass
KeyValue = namedtuple('KeyValue', ['key', 'value', 'sep', 'orig'])
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
def __eq__(self, other):
return self.__dict__ == other.__dict__
class KeyValueType(object):
"""A type used with `argparse`."""
key_value_class = KeyValue
def __init__(self, *separators):
self.separators = separators
self.escapes = ['\\\\' + sep for sep in separators]
@ -126,7 +221,44 @@ class KeyValueType(object):
for sepstr in self.separators:
key = key.replace('\\' + sepstr, sepstr)
value = value.replace('\\' + sepstr, sepstr)
return KeyValue(key=key, value=value, sep=sep, orig=string)
return self.key_value_class(key=key, value=value, sep=sep, orig=string)
class AuthCredentials(KeyValue):
"""
Represents parsed credentials.
"""
def _getpass(self, prompt):
# To allow mocking.
return getpass.getpass(prompt)
def has_password(self):
return self.value is not None
def prompt_password(self):
try:
self.value = self._getpass("Password for user '%s': " % self.key)
except (EOFError, KeyboardInterrupt):
sys.stderr.write('\n')
sys.exit(0)
class AuthCredentialsType(KeyValueType):
key_value_class = AuthCredentials
def __call__(self, string):
try:
return super(AuthCredentialsType, self).__call__(string)
except argparse.ArgumentTypeError:
# No password provided, will prompt for it later.
return self.key_value_class(
key=string,
value=None,
sep=SEP_COMMON,
orig=string
)
def parse_items(items, data=None, headers=None, files=None):

View File

@ -25,10 +25,15 @@ def from_request(request):
# requests < 0.12.1
body = request._enc_data
if isinstance(body, dict):
# --form
body = request.__class__._encode_params(body)
return HTTPMessage(
line='{method} {path} HTTP/1.1'.format(
line='{method} {path}{query} HTTP/1.1'.format(
method=request.method,
path=url.path or '/'),
path=url.path or '/',
query='' if url.query is '' else '?' + url.query),
headers='\n'.join(str('%s: %s') % (name, value)
for name, value
in request_headers.items()),

View File

@ -1,4 +1,5 @@
import os
import re
import json
import pygments
@ -18,6 +19,8 @@ FORMATTER = (Terminal256Formatter
if '256color' in os.environ.get('TERM', '')
else TerminalFormatter)
application_content_type_re = re.compile(r'application/(.+\+)?(json|xml)$')
class PrettyHttp(object):
@ -33,16 +36,23 @@ class PrettyHttp(object):
def body(self, content, content_type):
content_type = content_type.split(';')[0]
application_match = re.match(application_content_type_re, content_type)
if application_match:
# Strip vendor and extensions from Content-Type
vendor, extension = application_match.groups()
content_type = content_type.replace(vendor, u"")
try:
lexer = get_lexer_for_mimetype(content_type)
except ClassNotFound:
return content
if content_type == 'application/json':
if content_type == "application/json":
try:
# Indent and sort the JSON data.
content = json.dumps(json.loads(content),
sort_keys=True, indent=4)
sort_keys=True, indent=4,
ensure_ascii=False)
except:
pass

View File

@ -5,12 +5,14 @@ import httpie
if sys.argv[-1] == 'test':
os.system('python tests.py')
sys.exit()
sys.exit(os.system('python tests/tests.py'))
# Debian has only requests==0.10.1 and httpie.deb depends on that.
requirements = ['requests>=0.10.1', 'Pygments>=1.5']
requirements = [
# Debian has only requests==0.10.1 and httpie.deb depends on that.
'requests>=0.10.1',
'Pygments>=1.5'
]
if sys.version_info[:2] in ((2, 6), (3, 1)):
# argparse has been added in Python 3.2 / 2.7
requirements.append('argparse>=1.2.1')

1
tests/file2.txt Normal file
View File

@ -0,0 +1 @@
__test_file_content__

View File

@ -1,23 +1,37 @@
# coding:utf-8
import os
import sys
# -*- coding: utf-8 -*-
import unittest
import argparse
from requests.compat import is_py26
import os
import sys
import tempfile
import json
from requests.compat import is_py26
from requests import Response
#################################################################
# Utils/setup
#################################################################
# HACK: Prepend ../ to PYTHONPATH so that we can import httpie form there.
TESTS_ROOT = os.path.dirname(__file__)
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import __main__
from httpie import cliparse
from httpie import __main__, cliparse
TEST_FILE = os.path.join(TESTS_ROOT, 'file.txt')
TEST_FILE_PATH = os.path.join(TESTS_ROOT, 'file.txt')
TEST_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
def http(*args, **kwargs):
"""
Invoke `httpie.__main__.main` with `args` and `kwargs`,
and return a unicode response.
"""
http_kwargs = {
'stdin_isatty': True,
'stdout_isatty': False
@ -31,7 +45,7 @@ def http(*args, **kwargs):
return response
class BaseTest(unittest.TestCase):
class BaseTestCase(unittest.TestCase):
if is_py26:
def assertIn(self, member, container, msg=None):
@ -45,7 +59,252 @@ class BaseTest(unittest.TestCase):
self.assertEqual(sorted(d1.values()), sorted(d2.values()), msg)
class TestItemParsing(BaseTest):
#################################################################
# High-level tests using httpbin.org.
#################################################################
class HTTPieTest(BaseTestCase):
def test_GET(self):
r = http('GET', 'http://httpbin.org/get')
self.assertIn('HTTP/1.1 200', r)
def test_DELETE(self):
r = http('DELETE', 'http://httpbin.org/delete')
self.assertIn('HTTP/1.1 200', r)
def test_PUT(self):
r = http('PUT', 'http://httpbin.org/put', 'foo=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_POST_JSON_data(self):
r = http('POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_POST_form(self):
r = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_POST_stdin(self):
r = http('--form', 'POST', 'http://httpbin.org/post',
stdin=open(TEST_FILE_PATH), stdin_isatty=False)
self.assertIn('HTTP/1.1 200', r)
self.assertIn(TEST_FILE_CONTENT, r)
def test_headers(self):
r = http('GET', 'http://httpbin.org/headers', 'Foo:bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"User-Agent": "HTTPie', r)
self.assertIn('"Foo": "bar"', r)
class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
"""
Test that Accept and Content-Type correctly defaults to JSON,
but can still be overridden. The same with Content-Type when --form
-f is used.
"""
def test_GET_no_data_no_auto_headers(self):
# https://github.com/jkbr/httpie/issues/62
r = http('GET', 'http://httpbin.org/headers')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "*/*"', r)
# Although an empty header is present in the response from httpbin,
# it's not included in the request.
self.assertIn('"Content-Type": ""', r)
def test_POST_no_data_no_auto_headers(self):
# JSON headers shouldn't be automatically set for POST with no data.
r = http('POST', 'http://httpbin.org/post')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "*/*"', r)
# Although an empty header is present in the response from httpbin,
# it's not included in the request.
self.assertIn(' "Content-Type": ""', r)
def test_POST_with_data_auto_JSON_headers(self):
r = http('POST', 'http://httpbin.org/post', 'a=b')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
def test_GET_with_data_auto_JSON_headers(self):
# JSON headers should automatically be set also for GET with data.
r = http('POST', 'http://httpbin.org/post', 'a=b')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
def test_POST_explicit_JSON_auto_JSON_headers(self):
r = http('-j', 'POST', 'http://httpbin.org/post')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
def test_GET_explicit_JSON_explicit_headers(self):
r = http('-j', 'GET', 'http://httpbin.org/headers',
'Accept:application/xml',
'Content-Type:application/xml')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/xml"', r)
self.assertIn('"Content-Type": "application/xml"', r)
def test_POST_form_auto_Content_Type(self):
r = http('-f', 'POST', 'http://httpbin.org/post')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"', r)
def test_POST_form_Content_Type_override(self):
r = http('-f', 'POST', 'http://httpbin.org/post', 'Content-Type:application/xml')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Content-Type": "application/xml"', r)
class ImplicitHTTPMethodTest(BaseTestCase):
def test_implicit_GET(self):
r = http('http://httpbin.org/get')
self.assertIn('HTTP/1.1 200', r)
def test_implicit_GET_with_headers(self):
r = http('http://httpbin.org/headers', 'Foo:bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Foo": "bar"', r)
def test_implicit_POST_json(self):
r = http('http://httpbin.org/post', 'hello=world')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"hello": "world"', r)
def test_implicit_POST_form(self):
r = http('--form', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_implicit_POST_stdin(self):
r = http('--form', 'http://httpbin.org/post',
stdin=open(TEST_FILE_PATH), stdin_isatty=False)
self.assertIn('HTTP/1.1 200', r)
class PrettyFlagTest(BaseTestCase):
"""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_PRESENCE_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_PRESENCE_CHECK, r)
def test_force_pretty(self):
r = http('--pretty', 'GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_ugly(self):
r = http('--ugly', 'GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
class VerboseFlagTest(BaseTestCase):
def test_verbose(self):
r = http('--verbose', 'GET', 'http://httpbin.org/get', 'test-header:__test__')
self.assertIn('HTTP/1.1 200', r)
self.assertEqual(r.count('__test__'), 2)
def test_verbose_form(self):
# https://github.com/jkbr/httpie/issues/53
r = http('--verbose', '--form', 'POST', 'http://httpbin.org/post', 'foo=bar', 'baz=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('foo=bar&baz=bar', r)
class MultipartFormDataFileUploadTest(BaseTestCase):
def test_non_existent_file_raises_parse_error(self):
self.assertRaises(cliparse.ParseError, http,
'--form', '--traceback',
'POST', 'http://httpbin.org/post',
'foo@/__does_not_exist__')
def test_upload_ok(self):
r = http('--form', 'POST', 'http://httpbin.org/post',
'test-file@%s' % TEST_FILE_PATH, 'foo=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"test-file": "%s' % TEST_FILE_CONTENT, r)
self.assertIn('"foo": "bar"', r)
class RequestBodyFromFilePathTest(BaseTestCase):
"""
`http URL @file'
"""
def test_request_body_from_file_by_path(self):
r = http('POST', 'http://httpbin.org/post', '@' + TEST_FILE_PATH)
self.assertIn('HTTP/1.1 200', r)
self.assertIn(TEST_FILE_CONTENT, r)
self.assertIn('"Content-Type": "text/plain"', r)
def test_request_body_from_file_by_path_with_explicit_content_type(self):
r = http('POST', 'http://httpbin.org/post', '@' + TEST_FILE_PATH, 'Content-Type:x-foo/bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn(TEST_FILE_CONTENT, r)
self.assertIn('"Content-Type": "x-foo/bar"', r)
def test_request_body_from_file_by_path_only_one_file_allowed(self):
self.assertRaises(SystemExit, lambda: http(
'POST',
'http://httpbin.org/post',
'@' + TEST_FILE_PATH,
'@' + TEST_FILE2_PATH))
def test_request_body_from_file_by_path_no_data_items_allowed(self):
self.assertRaises(SystemExit, lambda: http(
'POST',
'http://httpbin.org/post',
'@' + TEST_FILE_PATH,
'foo=bar'))
class AuthTest(BaseTestCase):
def test_basic_auth(self):
r = http('--auth', 'user:password',
'GET', 'httpbin.org/basic-auth/user/password')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_digest_auth(self):
r = http('--auth-type=digest', '--auth', 'user:password',
'GET', 'httpbin.org/digest-auth/auth/user/password')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_password_prompt(self):
cliparse.AuthCredentials._getpass = lambda self, prompt: 'password'
r = http('--auth', 'user',
'GET', 'httpbin.org/basic-auth/user/password')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
#################################################################
# CLI argument parsing related tests.
#################################################################
class ItemParsingTest(BaseTestCase):
def setUp(self):
self.key_value_type = cliparse.KeyValueType(
@ -69,7 +328,7 @@ class TestItemParsing(BaseTest):
# data
self.key_value_type('baz\\=bar=foo'),
# files
self.key_value_type('bar\\@baz@%s' % TEST_FILE)
self.key_value_type('bar\\@baz@%s' % TEST_FILE_PATH)
])
self.assertDictEqual(headers, {
'foo:bar': 'baz',
@ -97,7 +356,7 @@ class TestItemParsing(BaseTest):
self.key_value_type('eh:'),
self.key_value_type('ed='),
self.key_value_type('bool:=true'),
self.key_value_type('test-file@%s' % TEST_FILE),
self.key_value_type('test-file@%s' % TEST_FILE_PATH),
])
self.assertDictEqual(headers, {
'header': 'value',
@ -113,80 +372,123 @@ class TestItemParsing(BaseTest):
self.assertIn('test-file', files)
class TestHTTPie(BaseTest):
class ArgumentParserTestCase(unittest.TestCase):
def test_get(self):
http('GET', 'http://httpbin.org/get')
def setUp(self):
self.parser = cliparse.Parser()
def test_verbose(self):
r = http('--verbose', 'GET', 'http://httpbin.org/get', 'test-header:__test__')
self.assertEqual(r.count('__test__'), 2)
def test_guess_when_method_set_and_valid(self):
args = argparse.Namespace()
args.method = 'GET'
args.url = 'http://example.com/'
args.items = []
def test_json(self):
response = http('POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
response2 = http('-j', 'GET', 'http://httpbin.org/headers')
self.assertIn('"Accept": "application/json"', response2)
response3 = http('-j', 'GET', 'http://httpbin.org/headers', 'Accept:application/xml')
self.assertIn('"Accept": "application/xml"', response3)
self.parser._guess_method(args)
def test_form(self):
response = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
def test_headers(self):
response = http('GET', 'http://httpbin.org/headers', 'Foo:bar')
self.assertIn('"User-Agent": "HTTPie', response)
self.assertIn('"Foo": "bar"', response)
def test_guess_when_method_not_set(self):
args = argparse.Namespace()
args.method = None
args.url = 'http://example.com/'
args.items = []
self.parser._guess_method(args)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
def test_guess_when_method_set_but_invalid_and_data_field(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'data=field'
args.items = []
self.parser._guess_method(args)
self.assertEquals(args.method, 'POST')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(
args.items,
[cliparse.KeyValue(key='data', value='field', sep='=', orig='data=field')])
def test_guess_when_method_set_but_invalid_and_header_field(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'test:header'
args.items = []
self.parser._guess_method(args)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(
args.items,
[cliparse.KeyValue(key='test', value='header', sep=':', orig='test:header')])
def test_guess_when_method_set_but_invalid_and_item_exists(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'new_item=a'
args.items = [
cliparse.KeyValue(key='old_item', value='b', sep='=', orig='old_item=b')
]
self.parser._guess_method(args)
self.assertEquals(args.items, [
cliparse.KeyValue(key='new_item', value='a', sep='=', orig='new_item=a'),
cliparse.KeyValue(key='old_item', value='b', sep='=', orig='old_item=b'),
])
class TestPrettyFlag(BaseTest):
"""Test the --pretty / --ugly flag handling."""
class FakeResponse(Response):
def test_pretty_enabled_by_default(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
class Mock(object):
def test_pretty_enabled_by_default_unless_stdin_redirected(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def __getattr__(self, item):
return self
def test_force_pretty(self):
r = http('--pretty', 'GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def __repr__(self):
return u'Mock string'
def test_force_ugly(self):
r = http('--ugly', 'GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def __unicode__(self):
return self.__repr__()
def __init__(self, content=None, encoding='utf-8'):
super(FakeResponse, self).__init__()
self.headers['Content-Type'] = 'application/json'
self.encoding = encoding
self._content = content.encode(encoding)
self.raw = self.Mock()
class TestFileUpload(BaseTest):
class UnicodeOutputTestCase(BaseTestCase):
def test_non_existent_file_raises_parse_error(self):
self.assertRaises(cliparse.ParseError, http,
'--form', '--traceback',
'POST', 'http://httpbin.org/post',
'foo@/__does_not_exist__')
def test_unicode_output(self):
# some cyrillic and simplified chinese symbols
response_dict = {u'Привет': u'Мир!',
u'Hello': u'世界'}
response_body = json.dumps(response_dict)
# emulate response
response = FakeResponse(response_body)
def test_upload_ok(self):
r = http('--form', 'POST', 'http://httpbin.org/post',
'test-file@%s' % TEST_FILE)
self.assertIn('"test-file": "__test_file_content__', r)
# emulate cli arguments
args = argparse.Namespace()
args.prettify = True
args.output_options = 'b'
args.forced_content_type = None
args.style = 'default'
# colorized output contains escape sequences
output = __main__._get_output(args, True, response)
class TestAuth(BaseTest):
def test_basic_auth(self):
r = http('--auth', 'user:password',
'GET', 'httpbin.org/basic-auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_digest_auth(self):
r = http('--auth-type=digest', '--auth', 'user:password',
'GET', 'httpbin.org/digest-auth/auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
for key, value in response_dict.iteritems():
self.assertIn(key, output)
self.assertIn(value, output)
if __name__ == '__main__':

19
tox.ini Normal file
View File

@ -0,0 +1,19 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, py30, py31, py32, pypy
[testenv]
commands = {envpython} setup.py test
[testenv:py26]
deps = argparse
[testenv:py30]
deps = argparse
[testenv:py31]
deps = argparse