Compare commits

...

57 Commits
0.1.6 ... 0.2.1

Author SHA1 Message Date
a0700c41ad 0.2.1 2012-06-13 16:01:23 +02:00
e175fe9d0e Ensured a new line after the request message in the output. 2012-06-13 15:32:02 +02:00
d544ec3823 Made --verbose work also with requests<0.12.1. 2012-06-13 15:25:05 +02:00
6cf2910de0 Version bump to 0.2.1dev. 2012-06-13 15:24:48 +02:00
f64eb09571 Merge pull request #50 from dair-targ/master
Fixed --verbose flag for newer requests.
2012-06-13 06:14:12 -07:00
126130455e Merge pull request #45 from gandaro/pygments-lexers
Use the Pygments HTTP and JSON lexers
2012-06-13 06:09:11 -07:00
70b3658004 --verbose flag was not working. Here is bugfix. 2012-06-02 23:14:21 +04:00
d89eeb0796 PEP-8 2012-04-28 14:18:59 +02:00
bced559496 use PrettyHttp class; working --headers and --body 2012-04-28 14:16:47 +02:00
4aa86cb438 Use the full capability of HttpLexer 2012-04-26 14:48:38 +02:00
2d7f2c65a2 Use the Pygments HTTP and JSON lexers 2012-04-26 13:05:59 +02:00
3d11042772 Fixed classifiers in setup.py. 2012-04-25 02:16:10 +02:00
b8cc7c205c Updated README. 2012-04-25 02:13:39 +02:00
3a96706e18 0.2.0 2012-04-25 02:11:19 +02:00
7910269996 Updated README and docs. 2012-04-25 02:10:58 +02:00
c5b1aaaa28 Major clean-up and refactoring. 2012-04-25 01:32:53 +02:00
67d6426360 Fixed several unicode-related issues
Closes #31 Python 3 & non-ascii arguments => UnicodeEncodeError
Closes #41 Unicode response error.
Closes #42 UnicodeEncodeError when piping Unicode output
2012-04-25 00:08:40 +02:00
29e594daaf Merge pull request #44 from jakebasile/master
Escaping separators
2012-04-24 07:25:17 -07:00
90af1f7422 Fixed escaping for long separators. 2012-04-18 18:18:00 -05:00
16df8848e8 Removed accidentally included old funky code. 2012-04-16 20:47:13 -05:00
c29981c633 Added ability to escape parameters... except for the := ones. 2012-04-16 20:28:08 -05:00
6db93b25d8 Merge pull request #43 from jakebasile/master
-j/--json sets Accept header
2012-04-14 14:21:08 -07:00
45ce446017 -j/--json now adds "Accept": "application/json" to GET requests if no previous Accept header exists. 2012-04-14 14:13:53 -05:00
4da3821bc4 Lowered the minimum version of requests required
So that the Debian package works out of the box:
http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=663506
2012-04-11 22:21:11 +02:00
0c4c6c4753 Added --auth-type and tests for basic/digest auth.
Closes #38.
2012-04-11 13:56:25 +02:00
ee598d304d Merge branch 'digest-auth' of https://github.com/dshafik/httpie into dshafik-digest-auth 2012-04-11 13:16:26 +02:00
c6c1489212 Refactored @mmb's fix to --verify; updated docs.
Closes #32.
2012-04-11 12:46:08 +02:00
71d21d1feb make --verify no command line argument work 2012-04-05 12:55:05 -04:00
153663cb92 Add support for Digest authentication 2012-03-22 10:51:33 -04:00
1c5fd18465 Python 3 fixes (travis config). 2012-03-15 00:31:12 +01:00
ab23037582 Python 3 fixes. 2012-03-15 00:28:15 +01:00
3dbb61a8ca Added argparse for Python 3.1. 2012-03-15 00:22:29 +01:00
51aa0409e6 Added Python 3 support
Closes #11.
2012-03-15 00:13:11 +01:00
1f49900db6 Improved README. 2012-03-14 22:55:09 +01:00
e2e749b2a6 Improved request formatting. 2012-03-14 22:45:35 +01:00
ccbea8c96e Assume "/" as the Request-URI for printing when none present. 2012-03-14 19:30:12 +01:00
6a1f0248e1 Fixed tests. 2012-03-14 19:21:47 +01:00
b7e0473d6c Added file upload support
It is now possible to send multipart/form-data requests.

Note that the --file option used previously has been removed
because it didn't allow you specify the field name.

Example:

    http -f POST example.com field-name@/path/to/file
2012-03-14 19:14:37 +01:00
578acacdf3 Added a --verbose / -v flag
When set, the whole request as well as the response is printed. Shortcut for --print=HBhb.
2012-03-14 11:15:21 +01:00
ed888a2657 Made sure request Host is correct when printing. 2012-03-14 01:12:10 +01:00
5e19e1b95d Added a "New in development version" link. 2012-03-14 00:58:05 +01:00
02622a4135 Added the option to print the request
It is now possible to print any combination of the following
request-response bits:

    - Request headers (H)
    - Request body (B)
    - Response headers (h)
    - Response body (b)

The output is controlled by the --print / -p option which
defaults to "hb" (i.e., response headers and response body).

Note that -p was previously shortcut for --prety.

Closes #29.
2012-03-14 00:44:13 +01:00
31c28807c9 Added better JSON highlighting
A JSON-specific lexer for Pygments by Norman Richards (@orb)
has been added. It attempts to provide more interesting syntax
highlighting which correctly distinguishes between attribute
names and values.

Closes #25.
2012-03-13 21:45:40 +01:00
78e20c6e85 Merge remote-tracking branch 'upstream/master' 2012-03-13 11:00:56 -04:00
20408e12e9 Added BSD license text
HTTPie is going to be packaged for Debian and this will make it easier. The license and © is still the same as before.
2012-03-13 10:59:29 -04:00
3c8af4c170 Added BSD license text
HTTPie is going to be packaged for Debian and this will make it easier. The license and © is still the same as before.
2012-03-09 14:20:26 +01:00
c9eb2255f6 Added proper JSON highlighting 2012-03-05 12:58:21 -05:00
028d16d0ff Merge pull request #24 from faulkner/fix-packaging
Update references to moved README.
Closes #23, #24.
2012-03-05 00:04:53 -08:00
f5d4addab2 Update references to moved README. 2012-03-04 22:26:23 -08:00
ccb2aaf94f Update README.rst 2012-03-05 02:54:34 +01:00
8cff0a3e67 Updated README. 2012-03-05 00:48:06 +01:00
ce952c9e90 Added support for more 256 color terminals.
Closes #20. Thanks, @laurentb.
2012-03-04 16:40:02 +01:00
e83e601f7c Added a link to contributors on GitHub to README. 2012-03-04 16:39:57 +01:00
f995c61892 Merge pull request #21 from laurentb/packaging
Packaging fixes
2012-03-04 07:35:24 -08:00
71771dc4a6 One argument per line 2012-03-04 16:28:30 +01:00
1517f3d149 Include README.md in the source tarball
This should fix the IOError that occured in setup.py.
2012-03-04 16:28:30 +01:00
d9abf7d31c Updated screenshot. 2012-03-04 15:37:04 +01:00
17 changed files with 878 additions and 452 deletions

View File

@ -2,10 +2,10 @@ language: python
python: python:
- 2.6 - 2.6
- 2.7 - 2.7
# TODO: Python 3 - 3.1
#- 3.2 - 3.2
script: python tests.py script: python tests/tests.py
install: install:
- pip install requests pygments - pip install requests pygments
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install argparse; fi" - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]] || [[ $TRAVIS_PYTHON_VERSION == '3.1' ]]; then pip install argparse; fi"

26
LICENSE Normal file
View File

@ -0,0 +1,26 @@
Copyright © 2012 Jakub Roztocil <jakub@roztocil.name>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of The author nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR AND CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include README.rst LICENSE

107
README.md
View File

@ -1,107 +0,0 @@
## HTTPie: cURL for humans
[![Build Status](https://secure.travis-ci.org/jkbr/httpie.png)](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).
![httpie](https://github.com/jkbr/httpie/raw/master/httpie.png)
### Installation
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
http [flags] METHOD URL [header:value | data-field-name=value]*
The default request `Content-Type` is `application/json` and data fields are automatically serialized as a JSON `Object`, so this:
http PATCH api.example.com/person/1 X-API-Token:123 name=John email=john@example.org
Will issue the following request:
PATCH /person/1 HTTP/1.1
User-Agent: HTTPie/0.1
X-API-Token: 123
Content-Type: application/json; charset=utf-8
{"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`:
http PUT api.example.com/person/1 X-API-Token:123 < person.json
Most of the flags mirror the arguments you would use with `requests.request`. See `http -h`:
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 [items [items ...]]
HTTPie - cURL for humans.
positional arguments:
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.
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 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
certs. You can also set the REQUESTS_CA_BUNDLE
environment variable.
--proxy PROXY String mapping protocol to the URL of the proxy (e.g.
http:foo.bar:3128).
--allow-redirects Set this flag if full redirects are allowed (e.g. re-
POST-ing of data at new ``Location``)
--file PATH File to multipart upload
--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)

205
README.rst Normal file
View File

@ -0,0 +1,205 @@
HTTPie: cURL for humans
=======================
**HTTPie is a CLI HTTP utility** built out of frustration with existing tools. The goal is to make CLI interaction with HTTP-based services as human-friendly as possible.
HTTPie does so by providing an ``http`` command that allows for issuing arbitrary HTTP requests using a **simple and natural syntax** and displaying **colorized responses**:
.. image:: https://github.com/jkbr/httpie/raw/master/httpie.png
:alt: HTTPie compared to cURL
Under the hood, HTTPie uses the excellent `Requests <http://python-requests.org>`_ and `Pygments <http://pygments.org/>`_ Python libraries. Python 2.6+ is supported (including 3.x).
Installation
------------
The latest **stable version** of HTTPie can always be installed (or updated to) via `pip <http://www.pip-installer.org/en/latest/index.html>`_::
pip install -U httpie
Or, you can install the **development version** directly from GitHub:
.. image:: https://secure.travis-ci.org/jkbr/httpie.png
:target: http://travis-ci.org/jkbr/httpie
:alt: Build Status of the master branch
::
pip install -U https://github.com/jkbr/httpie/tarball/master
Usage
-----
Hello world::
http GET httpie.org
Synopsis::
http [flags] METHOD URL [items]
There are four types of key-value pair items available:
Headers (``Name:Value``)
Arbitrary HTTP headers. The ``:`` character is used to separate a header's name from its value, e.g., ``X-API-Token:123``.
Simple data fields (``field=value``)
Data items are included in the request body. Depending on the ``Content-Type``, they are automatically serialized as a JSON ``Object`` (default) or ``application/x-www-form-urlencoded`` (the ``-f`` flag). Data items use ``=`` as the separator, e.g., ``hello=world``.
Raw JSON fields (``field:=value``)
This item type is needed when ``Content-Type`` is JSON and a field's value is a ``Boolean``, ``Number``, nested ``Object`` or an ``Array``, because simple data items are always serialized as ``String``. E.g. ``pies:=[1,2,3]``.
File fields (``field@/path/to/file``)
Only available with ``-f`` / ``--form``. Use ``@`` as the separator, e.g., ``screenshot@/path/to/file.png``. The presence of a file field results into a ``multipart/form-data`` request.
Examples
^^^^^^^^
::
http PATCH api.example.com/person/1 X-API-Token:123 name=John email=john@example.org age:=29
The following request is issued::
PATCH /person/1 HTTP/1.1
User-Agent: HTTPie/0.1
X-API-Token: 123
Content-Type: application/json; charset=utf-8
{"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::
PATCH /person/1 HTTP/1.1
User-Agent: HTTPie/0.1
X-API-Token: 123
Content-Type: application/x-www-form-urlencoded; charset=utf-8
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::
http -f POST example.com/jobs name=John cv@~/Documents/cv.pdf
The above will send the same request as if the following HTML form were submitted::
<form enctype="multipart/form-data" method="post" action="http://example.com/jobs">
<input type="text" name="name" />
<input type="file" name="cv" />
</form>
A whole request body can be passed in via ``stdin`` instead::
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
Flags
^^^^^
Most of the flags mirror the arguments understood by ``requests.request``. See ``http -h`` for more details::
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 ...]]
HTTPie - cURL for humans. <http://httpie.org>
positional arguments:
METHOD The HTTP method to be used for the request (GET, POST,
PUT, DELETE, PATCH, ...).
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@/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.
--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
flag ensures prettifying even when stdout is
redirected.
--ugly, -u Do not prettify the response.
--print OUTPUT_OPTIONS, -p OUTPUT_OPTIONS
String specifying what should the output contain. "H"
stands for the request headers and "B" for the request
body. "h" stands for the response headers and "b" for
response the body. Defaults to "hb" which means that
the whole response (headers and body) is printed.
--verbose, -v Print the whole request as well as the response.
Shortcut for --print=HBhb.
--headers, -t Print only the response headers. Shortcut for
--print=h.
--body, -b Print only the response body. Shortcut for --print=b.
--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
--auth-type {basic,digest}
The authentication mechanism to be used. Defaults to
"basic".
--verify VERIFY Set to "no" to skip checking 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. Defaults to
"yes".
--proxy PROXY String mapping protocol to the URL of the proxy (e.g.
http:foo.bar:3128).
--allow-redirects Set this flag if full redirects are allowed (e.g. re-
POST-ing of data at new ``Location``)
--timeout TIMEOUT Float describes the timeout of the request (Use
socket.setdefaulttimeout() as fallback).
Contributors
------------
`View contributors on GitHub <https://github.com/jkbr/httpie/contributors>`_.
Changelog
---------
* `New in development version <https://github.com/jkbr/httpie/compare/0.2.1...master>`_
* 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)
* 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.
* Added file upload support (``http -f POST file_field_name@/path/to/file``).
* 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

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

View File

@ -1,75 +1,67 @@
#!/usr/bin/env python #!/usr/bin/env python
import os
import sys import sys
import json import json
import requests import requests
try:
from collections import OrderedDict from requests.compat import str
except ImportError:
OrderedDict = dict from . import httpmessage
from requests.structures import CaseInsensitiveDict from . import cliparse
from . import cli from . import cli
from . import pretty from . import pretty
from . import __version__ as version
DEFAULT_UA = 'HTTPie/%s' % version
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8' TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
TYPE_JSON = 'application/json; charset=utf-8' TYPE_JSON = 'application/json; charset=utf-8'
def main(args=None, def _get_response(parser, args, stdin, stdin_isatty):
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 not stdin_isatty:
if data: if args.data:
parser.error('Request body (stdin) and request ' parser.error('Request body (stdin) and request '
'data (key=value) cannot be mixed.') 'data (key=value) cannot be mixed.')
data = stdin.read() args.data = stdin.read()
# JSON/Form content type. if args.json or (not args.form and args.data):
if args.json or (not args.form and data): # 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: if stdin_isatty:
data = json.dumps(data) # Serialize the parsed data.
if 'Content-Type' not in headers and (data or args.json): args.data = json.dumps(args.data)
headers['Content-Type'] = TYPE_JSON if 'Accept' not in args.headers:
elif 'Content-Type' not in headers: # Default Accept to JSON as well.
headers['Content-Type'] = TYPE_FORM args.headers['Accept'] = 'application/json'
elif not args.files and 'Content-Type' not in args.headers:
# Form
args.headers['Content-Type'] = TYPE_FORM
# Fire the request. # Fire the request.
try: try:
response = requests.request( credentials = None
if args.auth:
auth_type = (requests.auth.HTTPDigestAuth
if args.auth_type == 'digest'
else requests.auth.HTTPBasicAuth)
credentials = auth_type(args.auth.key, args.auth.value)
return requests.request(
method=args.method.lower(), method=args.method.lower(),
url=args.url if '://' in args.url else 'http://%s' % args.url, url=args.url if '://' in args.url else 'http://%s' % args.url,
headers=headers, headers=args.headers,
data=data, data=args.data,
verify=True if args.verify == 'yes' else args.verify, verify={'yes': True, 'no': False}.get(args.verify, args.verify),
timeout=args.timeout, timeout=args.timeout,
auth=(args.auth.key, args.auth.value) if args.auth else None, auth=credentials,
proxies=dict((p.key, p.value) for p in args.proxy), proxies=dict((p.key, p.value) for p in args.proxy),
files=dict((os.path.basename(f.name), f) for f in args.file), files=args.files,
allow_redirects=args.allow_redirects, allow_redirects=args.allow_redirects,
) )
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
sys.stderr.write('\n') sys.stderr.write('\n')
sys.exit(1) sys.exit(1)
@ -79,37 +71,55 @@ def main(args=None,
sys.stderr.write(str(e.message) + '\n') sys.stderr.write(str(e.message) + '\n')
sys.exit(1) 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: def _get_output(args, stdout_isatty, response):
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. do_prettify = (args.prettify is True or
# TODO: preserve leading/trailing whitespaces in the body. (args.prettify == cliparse.PRETTIFY_STDOUT_TTY_ONLY
# Some of the Pygments styles add superfluous line breaks. and stdout_isatty))
if args.print_headers:
stdout.write(status_line.strip()) do_output_request = (cliparse.OUT_REQ_HEADERS in args.output_options
stdout.write('\n') or cliparse.OUT_REQ_BODY in args.output_options)
stdout.write(headers.strip().encode('utf-8'))
stdout.write('\n\n') do_output_response = (cliparse.OUT_RESP_HEADERS in args.output_options
if args.print_body: or cliparse.OUT_RESP_BODY in args.output_options)
stdout.write(body.strip().encode('utf-8'))
stdout.write('\n') prettifier = pretty.PrettyHttp(args.style) if do_prettify else None
output = []
if do_output_request:
output.append(httpmessage.format(
message=httpmessage.from_request(response.request),
prettifier=prettifier,
with_headers=cliparse.OUT_REQ_HEADERS in args.output_options,
with_body=cliparse.OUT_REQ_BODY in args.output_options
))
output.append('\n')
if do_output_response:
output.append('\n')
if do_output_response:
output.append(httpmessage.format(
message=httpmessage.from_response(response),
prettifier=prettifier,
with_headers=cliparse.OUT_RESP_HEADERS in args.output_options,
with_body=cliparse.OUT_RESP_BODY in args.output_options
))
output.append('\n')
return ''.join(output)
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)
output = _get_output(args, stdout_isatty, response)
output_bytes = output.encode('utf8')
f = (stdout.buffer if hasattr(stdout, 'buffer') else stdout)
f.write(output_bytes)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,70 +1,11 @@
import json """
import argparse CLI definition.
from collections import namedtuple
"""
from . import pretty from . import pretty
from . import __doc__ as doc from . import __doc__
from . import __version__ as version from . import __version__
from . import cliparse
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): def _(text):
@ -72,8 +13,10 @@ def _(text):
return ' '.join(text.strip().split()) return ' '.join(text.strip().split())
parser = argparse.ArgumentParser(description=doc.strip(),) desc = '%s <http://httpie.org>'
parser.add_argument('--version', action='version', version=version) parser = cliparse.HTTPieArgumentParser(description=desc % __doc__.strip(),)
parser.add_argument('--version', action='version', version=__version__)
# Content type. # Content type.
############################################# #############################################
@ -82,16 +25,17 @@ group_type = parser.add_mutually_exclusive_group(required=False)
group_type.add_argument( group_type.add_argument(
'--json', '-j', action='store_true', '--json', '-j', action='store_true',
help=_(''' help=_('''
Serialize data items as a JSON object and set (default) Data items are serialized as a JSON object.
Content-Type to application/json, if not specified. The Content-Type and Accept headers
are set to application/json (if not set via the command line).
''') ''')
) )
group_type.add_argument( group_type.add_argument(
'--form', '-f', action='store_true', '--form', '-f', action='store_true',
help=_(''' help=_('''
Serialize data items as form values and set Data items are serialized as form fields.
Content-Type to application/x-www-form-urlencoded, The Content-Type is set to application/x-www-form-urlencoded (if not specifid).
if not specified. The presence of any file fields results into a multipart/form-data request.
''') ''')
) )
@ -108,13 +52,12 @@ parser.add_argument(
prettify = parser.add_mutually_exclusive_group(required=False) prettify = parser.add_mutually_exclusive_group(required=False)
prettify.add_argument( prettify.add_argument(
'--pretty', '-p', dest='prettify', action='store_true', '--pretty', dest='prettify', action='store_true',
default=PRETTIFY_STDOUT_TTY_ONLY, default=cliparse.PRETTIFY_STDOUT_TTY_ONLY,
help=_(''' help=_('''
If stdout is a terminal, If stdout is a terminal, the response is prettified
the response is prettified by default (colorized and by default (colorized and indented if it is JSON).
indented if it is JSON). This flag ensures This flag ensures prettifying even when stdout is redirected.
prettifying even when stdout is redirected.
''') ''')
) )
prettify.add_argument( prettify.add_argument(
@ -124,46 +67,84 @@ prettify.add_argument(
''') ''')
) )
only = parser.add_mutually_exclusive_group(required=False) output_options = parser.add_mutually_exclusive_group(required=False)
only.add_argument( output_options.add_argument('--print', '-p', dest='output_options',
'--headers', '-t', dest='print_body', default=cliparse.OUT_RESP_HEADERS + cliparse.OUT_RESP_BODY,
action='store_false', default=True, help=_('''
help=(''' String specifying what should the output contain.
"{request_headers}" stands for the request headers and
"{request_body}" for the request body.
"{response_headers}" stands for the response headers and
"{response_body}" for response the body.
Defaults to "hb" which means that the whole response
(headers and body) is printed.
'''.format(
request_headers=cliparse.OUT_REQ_HEADERS,
request_body=cliparse.OUT_REQ_BODY,
response_headers=cliparse.OUT_RESP_HEADERS,
response_body=cliparse.OUT_RESP_BODY,
))
)
output_options.add_argument(
'--verbose', '-v', dest='output_options',
action='store_const', const=''.join(cliparse.OUTPUT_OPTIONS),
help=_('''
Print the whole request as well as the response.
Shortcut for --print={0}.
'''.format(''.join(cliparse.OUTPUT_OPTIONS)))
)
output_options.add_argument(
'--headers', '-t', dest='output_options',
action='store_const', const=cliparse.OUT_RESP_HEADERS,
help=_('''
Print only the response headers. Print only the response headers.
''') Shortcut for --print={0}.
'''.format(cliparse.OUT_RESP_HEADERS))
) )
only.add_argument( output_options.add_argument(
'--body', '-b', dest='print_headers', '--body', '-b', dest='output_options',
action='store_false', default=True, action='store_const', const=cliparse.OUT_RESP_BODY,
help=(''' help=_('''
Print only the response body. Print only the response body.
''') Shortcut for --print={0}.
'''.format(cliparse.OUT_RESP_BODY))
) )
parser.add_argument( parser.add_argument(
'--style', '-s', dest='style', default='solarized', metavar='STYLE', '--style', '-s', dest='style', default='solarized', metavar='STYLE',
choices=pretty.AVAILABLE_STYLES, choices=pretty.AVAILABLE_STYLES,
help=_(''' help=_('''
Output coloring style, one of %s. Defaults to solarized. Output coloring style, one of %s. 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).
''') % ', '.join(sorted(pretty.AVAILABLE_STYLES)) ''') % ', '.join(sorted(pretty.AVAILABLE_STYLES))
) )
# ``requests.request`` keyword arguments. # ``requests.request`` keyword arguments.
parser.add_argument( parser.add_argument(
'--auth', '-a', help='username:password', '--auth', '-a', help='username:password',
type=KeyValueType(SEP_COMMON) type=cliparse.KeyValueType(cliparse.SEP_COMMON)
) )
parser.add_argument( parser.add_argument(
'--verify', '--auth-type', choices=['basic', 'digest'],
help=_('The authentication mechanism to be used. Defaults to "basic".')
)
parser.add_argument(
'--verify', default='yes',
help=_(''' help=_('''
Set to "yes" to check the host\'s SSL certificate. Set to "no" to skip checking the host\'s SSL certificate.
You can also pass the path to a CA_BUNDLE You can also pass the path to a CA_BUNDLE
file for private certs. You can also set file for private certs. You can also set
the REQUESTS_CA_BUNDLE environment variable. the REQUESTS_CA_BUNDLE environment variable.
Defaults to "yes".
''') ''')
) )
parser.add_argument( parser.add_argument(
'--proxy', default=[], action='append', '--proxy', default=[], action='append',
type=KeyValueType(SEP_COMMON), type=cliparse.KeyValueType(cliparse.SEP_COMMON),
help=_(''' help=_('''
String mapping protocol to the URL of the proxy String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128). (e.g. http:foo.bar:3128).
@ -176,11 +157,6 @@ parser.add_argument(
(e.g. re-POST-ing of data at new ``Location``) (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( parser.add_argument(
'--timeout', type=float, '--timeout', type=float,
help=_(''' help=_('''
@ -196,22 +172,32 @@ parser.add_argument(
parser.add_argument( parser.add_argument(
'method', metavar='METHOD', 'method', metavar='METHOD',
help=_(''' help=_('''
HTTP method to be used for the request The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...). (GET, POST, PUT, DELETE, PATCH, ...).
''') ''')
) )
parser.add_argument( parser.add_argument(
'url', metavar='URL', 'url', metavar='URL',
help=_(''' help=_('''
Protocol defaults to http:// if the The protocol defaults to http:// if the
URL does not include it. URL does not include one.
''') ''')
) )
parser.add_argument( parser.add_argument(
'items', nargs='*', 'items', nargs='*',
type=KeyValueType(SEP_COMMON, SEP_DATA, SEP_DATA_RAW_JSON), metavar='ITEM',
type=cliparse.KeyValueType(
cliparse.SEP_COMMON,
cliparse.SEP_DATA,
cliparse.SEP_DATA_RAW_JSON,
cliparse.SEP_FILES
),
help=_(''' help=_('''
HTTP header (key:value), data field (key=value) A key-value pair whose type is defined by the separator used. It can be an
or raw JSON field (field:=value). 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@/path/to/file).
You can use a backslash to escape a colliding separator in the field name.
''') ''')
) )

169
httpie/cliparse.py Normal file
View File

@ -0,0 +1,169 @@
"""
CLI argument parsing logic.
"""
import os
import re
import json
import argparse
from collections import namedtuple
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
from requests.structures import CaseInsensitiveDict
from . import __version__
SEP_COMMON = ':'
SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
OUT_REQ_HEADERS = 'H'
OUT_REQ_BODY = 'B'
OUT_RESP_HEADERS = 'h'
OUT_RESP_BODY = 'b'
OUTPUT_OPTIONS = [OUT_REQ_HEADERS,
OUT_REQ_BODY,
OUT_RESP_HEADERS,
OUT_RESP_BODY]
PRETTIFY_STDOUT_TTY_ONLY = object()
DEFAULT_UA = 'HTTPie/%s' % __version__
class HTTPieArgumentParser(argparse.ArgumentParser):
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._parse_items(args)
return args
def _parse_items(self, args):
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = OrderedDict()
args.files = OrderedDict()
try:
parse_items(items=args.items, headers=args.headers,
data=args.data, files=args.files)
except ParseError as e:
if args.traceback:
raise
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()))
def _validate_output_options(self, args):
unknown_output_options = set(args.output_options) - set(OUTPUT_OPTIONS)
if unknown_output_options:
self.error('Unknown output options: %s' % ','.join(unknown_output_options))
def _validate_auth_options(self, args):
if args.auth_type and not args.auth:
self.error('--auth-type can only be used with --auth')
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
self.escapes = ['\\\\' + sep for sep in separators]
def __call__(self, string):
found = {}
found_escapes = []
for esc in self.escapes:
found_escapes += [m.span() for m in re.finditer(esc, string)]
for sep in self.separators:
matches = re.finditer(sep, string)
for match in matches:
start, end = match.span()
inside_escape = False
for estart, eend in found_escapes:
if start >= estart and end <= eend:
inside_escape = True
break
if not inside_escape:
found[start] = sep
if not found:
raise argparse.ArgumentTypeError(
'"%s" is not a valid value' % string)
# split the string at the earliest non-escaped separator.
seploc = min(found.keys())
sep = found[seploc]
key = string[:seploc]
value = string[seploc + len(sep):]
# remove escape chars
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)
def parse_items(items, data=None, headers=None, files=None):
"""Parse `KeyValueType` `items` into `data`, `headers` and `files`."""
if headers is None:
headers = {}
if data is None:
data = {}
if files is None:
files = {}
for item in items:
value = item.value
key = item.key
if item.sep == SEP_HEADERS:
target = headers
elif item.sep == SEP_FILES:
try:
value = open(os.path.expanduser(item.value), 'r')
except IOError as e:
raise ParseError(
'Invalid argument %r. %s' % (item.orig, e))
if not key:
key = os.path.basename(value.name)
target = files
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 key in target:
ParseError('duplicate item %s (%s)' % (item.key, item.orig))
target[key] = value
return headers, data, files

74
httpie/httpmessage.py Normal file
View File

@ -0,0 +1,74 @@
from requests.compat import urlparse
class HTTPMessage(object):
"""Model representing an HTTP message."""
def __init__(self, line, headers, body, content_type=None):
# {Request,Status}-Line
self.line = line
self.headers = headers
self.body = body
self.content_type = content_type
def from_request(request):
"""Make an `HTTPMessage` from `requests.models.Request`."""
url = urlparse(request.url)
request_headers = dict(request.headers)
if 'Host' not in request_headers:
request_headers['Host'] = url.netloc
try:
body = request.data
except AttributeError:
# requests < 0.12.1
body = request._enc_data
return HTTPMessage(
line='{method} {path} HTTP/1.1'.format(
method=request.method,
path=url.path or '/'),
headers='\n'.join(str('%s: %s') % (name, value)
for name, value
in request_headers.items()),
body=body,
content_type=request_headers.get('Content-Type')
)
def from_response(response):
"""Make an `HTTPMessage` from `requests.models.Response`."""
encoding = response.encoding or 'ISO-8859-1'
original = response.raw._original_response
response_headers = response.headers
return HTTPMessage(
line='HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)),
status=original.status, reason=original.reason),
headers=str(original.msg),
body=response.content.decode(encoding) if response.content else '',
content_type=response_headers.get('Content-Type'))
def format(message, prettifier=None,
with_headers=True, with_body=True):
"""Return a `unicode` representation of `message`. """
pretty = prettifier is not None
bits = []
if with_headers:
bits.append(message.line)
bits.append(message.headers)
if pretty:
bits = [prettifier.headers('\n'.join(bits))]
if with_body and message.body:
bits.append('\n')
if with_body and message.body:
if pretty and message.content_type:
bits.append(prettifier.body(message.body, message.content_type))
else:
bits.append(message.body)
return '\n'.join(bit.strip() for bit in bits)

View File

@ -1,37 +1,24 @@
import os import os
import json import json
import pygments import pygments
from pygments import token
from pygments.util import ClassNotFound from pygments.util import ClassNotFound
from pygments.lexers import get_lexer_for_mimetype from pygments.styles import get_style_by_name, STYLE_MAP
from pygments.lexers import get_lexer_for_mimetype, HttpLexer
from pygments.formatters.terminal256 import Terminal256Formatter from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.formatters.terminal import TerminalFormatter from pygments.formatters.terminal import TerminalFormatter
from pygments.lexer import RegexLexer, bygroups
from pygments.styles import get_style_by_name, STYLE_MAP
from . import solarized from . import solarized
DEFAULT_STYLE = 'solarized' DEFAULT_STYLE = 'solarized'
AVAILABLE_STYLES = [DEFAULT_STYLE] + STYLE_MAP.keys() AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys())
TYPE_JS = 'application/javascript'
FORMATTER = (Terminal256Formatter FORMATTER = (Terminal256Formatter
if os.environ.get('TERM') == 'xterm-256color' if '256color' in os.environ.get('TERM', '')
else TerminalFormatter) else TerminalFormatter)
class HTTPLexer(RegexLexer):
name = 'HTTP'
aliases = ['http']
filenames = ['*.http']
tokens = {
'root': [
(r'\s+', token.Text),
(r'(HTTP/[\d.]+\s+)(\d+)(\s+.+)', bygroups(
token.Operator, token.Number, token.String)),
(r'(.*?:)(.+)', bygroups(token.Name, token.String))
]}
class PrettyHttp(object): class PrettyHttp(object):
def __init__(self, style_name): def __init__(self, style_name):
@ -42,21 +29,21 @@ class PrettyHttp(object):
self.formatter = FORMATTER(style=style) self.formatter = FORMATTER(style=style)
def headers(self, content): def headers(self, content):
return pygments.highlight(content, HTTPLexer(), self.formatter) return pygments.highlight(content, HttpLexer(), self.formatter)
def body(self, content, content_type): def body(self, content, content_type):
content_type = content_type.split(';')[0] 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: try:
lexer = get_lexer_for_mimetype(content_type) lexer = get_lexer_for_mimetype(content_type)
except ClassNotFound: except ClassNotFound:
return content return content
content = pygments.highlight(content, lexer, self.formatter)
return content if content_type == 'application/json':
try:
# Indent and sort the JSON data.
content = json.dumps(json.loads(content),
sort_keys=True, indent=4)
except:
pass
return pygments.highlight(content, lexer, self.formatter)

View File

@ -27,7 +27,8 @@ THE SOFTWARE.
""" """
from pygments.style import Style from pygments.style import Style
from pygments.token import Token, Comment, Name, Keyword, Generic, Number, Operator, String from pygments.token import (Token, Comment, Name, Keyword, Generic, Number,
Operator, String)
BASE03 = '#002B36' BASE03 = '#002B36'

View File

@ -9,21 +9,18 @@ if sys.argv[-1] == 'test':
sys.exit() sys.exit()
requirements = ['requests>=0.10.4', 'Pygments>=1.4'] # Debian has only requests==0.10.1 and httpie.deb depends on that.
if sys.version_info < (2, 7): requirements = ['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') requirements.append('argparse>=1.2.1')
try:
long_description = open('README.md').read()
except IOError:
long_description = ''
setup( setup(
name='httpie',version=httpie.__version__, name='httpie',
version=httpie.__version__,
description=httpie.__doc__.strip(), description=httpie.__doc__.strip(),
long_description=long_description, long_description=open('README.rst').read(),
url='http://httpie.org/', url='http://httpie.org/',
download_url='https://github.com/jkbr/httpie', download_url='https://github.com/jkbr/httpie',
author=httpie.__author__, author=httpie.__author__,
@ -41,10 +38,8 @@ setup(
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
# TODO: Python 3 'Programming Language :: Python :: 3.1',
# 'Programming Language :: Python :: 3.1' 'Programming Language :: Python :: 3.2',
# 'Programming Language :: Python :: 3.2'
# 'Programming Language :: Python :: 3.3'
'Environment :: Console', 'Environment :: Console',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',

115
tests.py
View File

@ -1,115 +0,0 @@
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()

1
tests/file.txt Normal file
View File

@ -0,0 +1 @@
__test_file_content__

193
tests/tests.py Normal file
View File

@ -0,0 +1,193 @@
# coding:utf-8
import os
import sys
import unittest
import argparse
from requests.compat import is_py26
import tempfile
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
TEST_FILE = os.path.join(TESTS_ROOT, 'file.txt')
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
def http(*args, **kwargs):
http_kwargs = {
'stdin_isatty': True,
'stdout_isatty': False
}
http_kwargs.update(kwargs)
stdout = http_kwargs.setdefault('stdout', tempfile.TemporaryFile())
__main__.main(args=args, **http_kwargs)
stdout.seek(0)
response = stdout.read().decode('utf8')
stdout.close()
return response
class BaseTest(unittest.TestCase):
if is_py26:
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.key_value_type = cliparse.KeyValueType(
cliparse.SEP_HEADERS,
cliparse.SEP_DATA,
cliparse.SEP_DATA_RAW_JSON,
cliparse.SEP_FILES,
)
def test_invalid_items(self):
items = ['no-separator']
for item in items:
self.assertRaises(argparse.ArgumentTypeError,
lambda: self.key_value_type(item))
def test_escape(self):
headers, data, files = cliparse.parse_items([
# headers
self.key_value_type('foo\\:bar:baz'),
self.key_value_type('jack\\@jill:hill'),
# data
self.key_value_type('baz\\=bar=foo'),
# files
self.key_value_type('bar\\@baz@%s' % TEST_FILE)
])
self.assertDictEqual(headers, {
'foo:bar': 'baz',
'jack@jill': 'hill',
})
self.assertDictEqual(data, {
'baz=bar': 'foo',
})
self.assertIn('bar@baz', files)
def test_escape_longsep(self):
headers, data, files = cliparse.parse_items([
self.key_value_type('bob\\:==foo'),
])
self.assertDictEqual(data, {
'bob:=': 'foo',
})
def test_valid_items(self):
headers, data, files = cliparse.parse_items([
self.key_value_type('string=value'),
self.key_value_type('header:value'),
self.key_value_type('list:=["a", 1, {}, false]'),
self.key_value_type('obj:={"a": "b"}'),
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.assertDictEqual(headers, {
'header': 'value',
'eh': ''
})
self.assertDictEqual(data, {
"ed": "",
"string": "value",
"bool": True,
"list": ["a", 1, {}, False],
"obj": {"a": "b"}
})
self.assertIn('test-file', files)
class TestHTTPie(BaseTest):
def test_get(self):
http('GET', 'http://httpbin.org/get')
def test_verbose(self):
r = http('--verbose', 'GET', 'http://httpbin.org/get', 'test-header:__test__')
self.assertEqual(r.count('__test__'), 2)
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)
def test_form(self):
response = http('--form', 'POST', '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_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 TestFileUpload(BaseTest):
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)
self.assertIn('"test-file": "__test_file_content__', r)
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)
if __name__ == '__main__':
unittest.main()