mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 19:45:42 +02:00
Compare commits
129 Commits
Author | SHA1 | Date | |
---|---|---|---|
9b8cb42efd | |||
5ca8bec9ff | |||
df79792fd9 | |||
5a82c79fdf | |||
05b321d38f | |||
681b652bf9 | |||
85b3a016eb | |||
929ead437a | |||
7bc2de2f9d | |||
cb7ead04e2 | |||
cd2ca41f48 | |||
c71de95505 | |||
6ab03b21b4 | |||
50196be0f2 | |||
41d640920c | |||
3179631603 | |||
2f7921091c | |||
180313d80c | |||
926d3f5caf | |||
4613d947a8 | |||
5a47f00bac | |||
0e1affbbc4 | |||
d920f20847 | |||
bca36f0464 | |||
78fff98712 | |||
e06c448a75 | |||
9cdbd6b0ec | |||
cbc6d02127 | |||
284a75fa2f | |||
b3ea273a21 | |||
0d129d5f69 | |||
1388206f1a | |||
28dbe9f76c | |||
a0700c41ad | |||
e175fe9d0e | |||
d544ec3823 | |||
6cf2910de0 | |||
f64eb09571 | |||
126130455e | |||
70b3658004 | |||
d89eeb0796 | |||
bced559496 | |||
4aa86cb438 | |||
2d7f2c65a2 | |||
3d11042772 | |||
b8cc7c205c | |||
3a96706e18 | |||
7910269996 | |||
c5b1aaaa28 | |||
67d6426360 | |||
29e594daaf | |||
90af1f7422 | |||
16df8848e8 | |||
c29981c633 | |||
6db93b25d8 | |||
45ce446017 | |||
4da3821bc4 | |||
0c4c6c4753 | |||
ee598d304d | |||
c6c1489212 | |||
71d21d1feb | |||
153663cb92 | |||
1c5fd18465 | |||
ab23037582 | |||
3dbb61a8ca | |||
51aa0409e6 | |||
1f49900db6 | |||
e2e749b2a6 | |||
ccbea8c96e | |||
6a1f0248e1 | |||
b7e0473d6c | |||
578acacdf3 | |||
ed888a2657 | |||
5e19e1b95d | |||
02622a4135 | |||
31c28807c9 | |||
78e20c6e85 | |||
20408e12e9 | |||
3c8af4c170 | |||
c9eb2255f6 | |||
028d16d0ff | |||
f5d4addab2 | |||
ccb2aaf94f | |||
8cff0a3e67 | |||
ce952c9e90 | |||
e83e601f7c | |||
f995c61892 | |||
71771dc4a6 | |||
1517f3d149 | |||
d9abf7d31c | |||
c446d756ab | |||
7ca6191902 | |||
ebb271334b | |||
bd9209f77a | |||
7d629b4d94 | |||
a44ef6c443 | |||
f4dde9d9b3 | |||
6d14097844 | |||
8a4f501706 | |||
6774998012 | |||
2195280a70 | |||
f5d5ec22af | |||
b728710760 | |||
715e1b1047 | |||
ca8779d879 | |||
5ff43b659f | |||
b802f2b960 | |||
73d0f9cd56 | |||
00312ead28 | |||
d02ac54130 | |||
81568cf91c | |||
6df9ff67eb | |||
5d3176115a | |||
81798ad537 | |||
dd8faecbf7 | |||
58f74fe14a | |||
84a0d4a35d | |||
d670513c9f | |||
860a851a4b | |||
9634dca7d8 | |||
bb653bf1a9 | |||
94c605fac1 | |||
3442a5d037 | |||
5cd40916fe | |||
ed3a491c81 | |||
d768959084 | |||
f934f4345e | |||
b752b59d92 | |||
553941c98d |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ dist
|
||||
httpie.egg-info
|
||||
build
|
||||
*.pyc
|
||||
.tox
|
||||
|
10
.travis.yml
Normal file
10
.travis.yml
Normal file
@ -0,0 +1,10 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- pypy
|
||||
- 3.1
|
||||
- 3.2
|
||||
script: python setup.py test
|
||||
install:
|
||||
- pip install . --use-mirrors
|
26
LICENSE
Normal file
26
LICENSE
Normal 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
1
MANIFEST.in
Normal file
@ -0,0 +1 @@
|
||||
include README.rst LICENSE
|
77
README.md
77
README.md
@ -1,77 +0,0 @@
|
||||
## HTTPie: cURL for humans
|
||||
|
||||
HTTPie is a CLI frontend for [python-requests](http://python-requests.org) built out of frustration. It provides an `http` command that can be used to easily issue HTTP requests. It is meant to be used by humans to interact with HTTP-based APIs and web servers. The response headers are colorized and the body is syntax-highlighed if its `Content-Type` is known to [Pygments](http://pygments.org/) (unless the output is redirected).
|
||||
|
||||

|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
pip install httpie
|
||||
|
||||
|
||||
### 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 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`:
|
||||
|
||||
$ http -h
|
||||
usage: http [-h] [--json | --form] [--traceback] [--ugly] [--headers | --body]
|
||||
[--auth AUTH] [--verify VERIFY] [--proxy PROXY]
|
||||
[--allow-redirects] [--file PATH] [--timeout TIMEOUT]
|
||||
method URL [item [item ...]]
|
||||
|
||||
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.
|
||||
item HTTP header (key:value) or data field (key=value)
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--json, -j Serialize data items as a JSON object and set Content-
|
||||
Type to application/json, if not specified.
|
||||
--form, -f Serialize data items as form values and set Content-
|
||||
Type to application/x-www-form-urlencoded, if not
|
||||
specified.
|
||||
--traceback Print a full exception traceback should one be raised
|
||||
by `requests`.
|
||||
--ugly, -u Do not prettify the response.
|
||||
--headers, -t Print only the response headers.
|
||||
--body, -b Print only the response body.
|
||||
--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).
|
||||
|
249
README.rst
Normal file
249
README.rst
Normal file
@ -0,0 +1,249 @@
|
||||
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
|
||||
|
||||
|
||||
There are packages available for `Ubuntu <http://packages.ubuntu.com/quantal/httpie>`_ and `Debian <http://packages.debian.org/wheezy/httpie>`_.
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Hello world::
|
||||
|
||||
http 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, 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 ...]]
|
||||
|
||||
HTTPie - cURL for humans. <http://httpie.org>
|
||||
|
||||
positional arguments:
|
||||
METHOD 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.
|
||||
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 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
|
||||
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, 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".
|
||||
--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).
|
||||
|
||||
|
||||
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
|
||||
---------
|
||||
|
||||
* `0.2.3 <https://github.com/jkbr/httpie/compare/0.2.2...0.2.3>`_ (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``.
|
||||
* `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.
|
||||
* 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.
|
||||
* `0.1.6 <https://github.com/jkbr/httpie/compare/0.1.4...0.1.6>`_ (2012-03-04)
|
BIN
httpie.png
BIN
httpie.png
Binary file not shown.
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 135 KiB |
@ -3,5 +3,5 @@ HTTPie - cURL for humans.
|
||||
|
||||
"""
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.1.4'
|
||||
__version__ = '0.2.3'
|
||||
__licence__ = 'BSD'
|
||||
|
130
httpie/__main__.py
Normal file
130
httpie/__main__.py
Normal file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from requests.compat import str
|
||||
|
||||
from . import httpmessage
|
||||
from . import cliparse
|
||||
from . import cli
|
||||
from . import pretty
|
||||
|
||||
|
||||
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
TYPE_JSON = 'application/json; charset=utf-8'
|
||||
|
||||
|
||||
def _get_response(args):
|
||||
|
||||
auto_json = args.data and not args.form
|
||||
if args.json or auto_json:
|
||||
# JSON
|
||||
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'
|
||||
|
||||
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
|
||||
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:
|
||||
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(),
|
||||
url=args.url if '://' in args.url else 'http://%s' % args.url,
|
||||
headers=args.headers,
|
||||
data=args.data,
|
||||
verify={'yes': True, 'no': False}.get(args.verify, args.verify),
|
||||
timeout=args.timeout,
|
||||
auth=credentials,
|
||||
proxies=dict((p.key, p.value) for p in args.proxy),
|
||||
files=args.files,
|
||||
allow_redirects=args.allow_redirects,
|
||||
)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
if args.traceback:
|
||||
raise
|
||||
sys.stderr.write(str(e.message) + '\n')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _get_output(args, stdout_isatty, response):
|
||||
|
||||
do_prettify = (args.prettify is True or
|
||||
(args.prettify == cliparse.PRETTIFY_STDOUT_TTY_ONLY
|
||||
and stdout_isatty))
|
||||
|
||||
do_output_request = (cliparse.OUT_REQ_HEADERS in args.output_options
|
||||
or cliparse.OUT_REQ_BODY in args.output_options)
|
||||
|
||||
do_output_response = (cliparse.OUT_RESP_HEADERS in args.output_options
|
||||
or cliparse.OUT_RESP_BODY in args.output_options)
|
||||
|
||||
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=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)
|
||||
f.write(output_bytes)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
210
httpie/cli.py
Normal file
210
httpie/cli.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""
|
||||
CLI definition.
|
||||
|
||||
"""
|
||||
from . import pretty
|
||||
from . import __doc__
|
||||
from . import __version__
|
||||
from . import cliparse
|
||||
|
||||
|
||||
def _(text):
|
||||
"""Normalize white space."""
|
||||
return ' '.join(text.strip().split())
|
||||
|
||||
|
||||
desc = '%s <http://httpie.org>'
|
||||
parser = cliparse.Parser(description=desc % __doc__.strip(),)
|
||||
parser.add_argument('--version', action='version', version=__version__)
|
||||
|
||||
|
||||
# Content type.
|
||||
#############################################
|
||||
|
||||
group_type = parser.add_mutually_exclusive_group(required=False)
|
||||
group_type.add_argument(
|
||||
'--json', '-j', action='store_true',
|
||||
help=_('''
|
||||
(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).
|
||||
''')
|
||||
)
|
||||
group_type.add_argument(
|
||||
'--form', '-f', action='store_true',
|
||||
help=_('''
|
||||
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.
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
# Output options.
|
||||
#############################################
|
||||
|
||||
parser.add_argument(
|
||||
'--traceback', action='store_true', default=False,
|
||||
help=_('''
|
||||
Print exception traceback should one occur.
|
||||
''')
|
||||
)
|
||||
|
||||
prettify = parser.add_mutually_exclusive_group(required=False)
|
||||
prettify.add_argument(
|
||||
'--pretty', dest='prettify', action='store_true',
|
||||
default=cliparse.PRETTIFY_STDOUT_TTY_ONLY,
|
||||
help=_('''
|
||||
If stdout is a terminal, the response is prettified
|
||||
by default (colorized and indented if it is JSON).
|
||||
This flag ensures prettifying even when stdout is redirected.
|
||||
''')
|
||||
)
|
||||
prettify.add_argument(
|
||||
'--ugly', '-u', dest='prettify', action='store_false',
|
||||
help=_('''
|
||||
Do not prettify the response.
|
||||
''')
|
||||
)
|
||||
|
||||
output_options = parser.add_mutually_exclusive_group(required=False)
|
||||
output_options.add_argument('--print', '-p', dest='output_options',
|
||||
default=cliparse.OUT_RESP_HEADERS + cliparse.OUT_RESP_BODY,
|
||||
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.
|
||||
Shortcut for --print={0}.
|
||||
'''.format(cliparse.OUT_RESP_HEADERS))
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--body', '-b', dest='output_options',
|
||||
action='store_const', const=cliparse.OUT_RESP_BODY,
|
||||
help=_('''
|
||||
Print only the response body.
|
||||
Shortcut for --print={0}.
|
||||
'''.format(cliparse.OUT_RESP_BODY))
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--style', '-s', dest='style', default='solarized', metavar='STYLE',
|
||||
choices=pretty.AVAILABLE_STYLES,
|
||||
help=_('''
|
||||
Output coloring style, one of %s. Defaults to solarized.
|
||||
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))
|
||||
)
|
||||
|
||||
# ``requests.request`` keyword arguments.
|
||||
parser.add_argument(
|
||||
'--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(
|
||||
'--auth-type', choices=['basic', 'digest'],
|
||||
help=_('The authentication mechanism to be used. Defaults to "basic".')
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--verify', default='yes',
|
||||
help=_('''
|
||||
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".
|
||||
''')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--proxy', default=[], action='append',
|
||||
type=cliparse.KeyValueType(cliparse.SEP_COMMON),
|
||||
help=_('''
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:foo.bar:3128).
|
||||
''')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--allow-redirects', default=False, action='store_true',
|
||||
help=_('''
|
||||
Set this flag if full redirects are allowed
|
||||
(e.g. re-POST-ing of data at new ``Location``)
|
||||
''')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timeout', type=float,
|
||||
help=_('''
|
||||
Float describes the timeout of the request
|
||||
(Use socket.setdefaulttimeout() as fallback).
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
# Positional arguments.
|
||||
#############################################
|
||||
|
||||
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(
|
||||
'url', metavar='URL',
|
||||
help=_('''
|
||||
The protocol defaults to http:// if the
|
||||
URL does not include one.
|
||||
''')
|
||||
)
|
||||
parser.add_argument(
|
||||
'items', nargs='*',
|
||||
metavar='ITEM',
|
||||
type=cliparse.KeyValueType(
|
||||
cliparse.SEP_COMMON,
|
||||
cliparse.SEP_DATA,
|
||||
cliparse.SEP_DATA_RAW_JSON,
|
||||
cliparse.SEP_FILES
|
||||
),
|
||||
help=_('''
|
||||
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.
|
||||
''')
|
||||
)
|
301
httpie/cliparse.py
Normal file
301
httpie/cliparse.py
Normal file
@ -0,0 +1,301 @@
|
||||
"""
|
||||
CLI argument parsing logic.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import argparse
|
||||
import mimetypes
|
||||
import getpass
|
||||
|
||||
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 = '@'
|
||||
DATA_ITEM_SEPARATORS = [
|
||||
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 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)
|
||||
|
||||
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()
|
||||
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:
|
||||
# `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)
|
||||
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
|
||||
|
||||
|
||||
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]
|
||||
|
||||
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 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):
|
||||
"""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
|
183
httpie/httpie.py
183
httpie/httpie.py
@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from collections import namedtuple
|
||||
import requests
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from . import pretty
|
||||
from . import __version__ as version
|
||||
from . import __doc__ as doc
|
||||
|
||||
|
||||
DEFAULT_UA = 'HTTPie/%s' % version
|
||||
SEP_COMMON = ':'
|
||||
SEP_DATA = '='
|
||||
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
TYPE_JSON = 'application/json; charset=utf-8'
|
||||
|
||||
|
||||
KeyValue = namedtuple('KeyValue', ['key', 'value', 'sep'])
|
||||
|
||||
|
||||
class KeyValueType(object):
|
||||
|
||||
def __init__(self, separators):
|
||||
self.separators = separators
|
||||
|
||||
def __call__(self, string):
|
||||
found = dict((string.find(sep), sep)
|
||||
for sep in self.separators
|
||||
if string.find(sep) != -1)
|
||||
|
||||
if not found:
|
||||
raise argparse.ArgumentTypeError(
|
||||
'"%s" is not a valid value' % string)
|
||||
sep = found[min(found.keys())]
|
||||
key, value = string.split(sep, 1)
|
||||
return KeyValue(key=key, value=value, sep=sep)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=doc.strip())
|
||||
|
||||
|
||||
# Content type.
|
||||
group_type = parser.add_mutually_exclusive_group(required=False)
|
||||
group_type.add_argument('--json', '-j', action='store_true',
|
||||
help='Serialize data items as a JSON object and set'
|
||||
' Content-Type to application/json, if not specified.')
|
||||
group_type.add_argument('--form', '-f', action='store_true',
|
||||
help='Serialize data items as form values and set'
|
||||
' Content-Type to application/x-www-form-urlencoded,'
|
||||
' if not specified.')
|
||||
|
||||
# Output options.
|
||||
parser.add_argument('--traceback', action='store_true', default=False,
|
||||
help='Print a full exception traceback should one'
|
||||
' be raised by `requests`.')
|
||||
parser.add_argument('--ugly', '-u', help='Do not prettify the response.',
|
||||
dest='prettify', action='store_false', default=True)
|
||||
group_only = parser.add_mutually_exclusive_group(required=False)
|
||||
group_only.add_argument('--headers', '-t', dest='print_body',
|
||||
action='store_false', default=True,
|
||||
help='Print only the response headers.')
|
||||
group_only.add_argument('--body', '-b', dest='print_headers',
|
||||
action='store_false', default=True,
|
||||
help='Print only the response body.')
|
||||
|
||||
|
||||
# ``requests.request`` keyword arguments.
|
||||
parser.add_argument('--auth', '-a', help='username:password',
|
||||
type=KeyValueType(SEP_COMMON))
|
||||
parser.add_argument('--verify',
|
||||
help='Set to "yes" to check the host\'s SSL certificate.'
|
||||
' You can also pass the path to a CA_BUNDLE'
|
||||
' file for private certs. You can also set '
|
||||
'the REQUESTS_CA_BUNDLE environment variable.')
|
||||
parser.add_argument('--proxy', default=[], action='append',
|
||||
type=KeyValueType(SEP_COMMON),
|
||||
help='String mapping protocol to the URL of the proxy'
|
||||
' (e.g. http:foo.bar:3128).')
|
||||
parser.add_argument('--allow-redirects', default=False, action='store_true',
|
||||
help='Set this flag if full redirects are allowed'
|
||||
' (e.g. re-POST-ing of data at new ``Location``)')
|
||||
parser.add_argument('--file', metavar='PATH', type=argparse.FileType(),
|
||||
default=[], action='append',
|
||||
help='File to multipart upload')
|
||||
parser.add_argument('--timeout', type=float,
|
||||
help='Float describes the timeout of the request'
|
||||
' (Use socket.setdefaulttimeout() as fallback).')
|
||||
|
||||
# Positional arguments.
|
||||
parser.add_argument('method',
|
||||
help='HTTP method to be used for the request'
|
||||
' (GET, POST, PUT, DELETE, PATCH, ...).')
|
||||
parser.add_argument('url', metavar='URL',
|
||||
help='Protocol defaults to http:// if the'
|
||||
' URL does not include it.')
|
||||
parser.add_argument('items', metavar='item', nargs='*',
|
||||
type=KeyValueType([SEP_COMMON, SEP_DATA]),
|
||||
help='HTTP header (key:value) or data field (key=value)')
|
||||
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse request headers and data from the command line.
|
||||
headers = CaseInsensitiveDict()
|
||||
headers['User-Agent'] = DEFAULT_UA
|
||||
data = {}
|
||||
for item in args.items:
|
||||
if item.sep == SEP_COMMON:
|
||||
target = headers
|
||||
else:
|
||||
if not sys.stdin.isatty():
|
||||
parser.error('Request body (stdin) and request '
|
||||
'data (key=value) cannot be mixed.')
|
||||
target = data
|
||||
target[item.key] = item.value
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.read()
|
||||
|
||||
# JSON/Form content type.
|
||||
if args.json or (not args.form and data):
|
||||
if sys.stdin.isatty():
|
||||
data = json.dumps(data)
|
||||
if 'Content-Type' not in headers and (data or args.json):
|
||||
headers['Content-Type'] = TYPE_JSON
|
||||
elif 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = TYPE_FORM
|
||||
|
||||
# Fire the request.
|
||||
try:
|
||||
response = requests.request(
|
||||
method=args.method.lower(),
|
||||
url=args.url if '://' in args.url else 'http://%s' % args.url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
verify=True if args.verify == 'yes' else args.verify,
|
||||
timeout=args.timeout,
|
||||
auth=(args.auth.key, args.auth.value) if args.auth else None,
|
||||
proxies=dict((p.key, p.value) for p in args.proxy),
|
||||
files=dict((os.path.basename(f.name), f) for f in args.file),
|
||||
)
|
||||
except (KeyboardInterrupt, SystemExit) as e:
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
if args.traceback:
|
||||
raise
|
||||
sys.stderr.write(str(e.message) + '\n')
|
||||
sys.exit(1)
|
||||
|
||||
# Display the response.
|
||||
encoding = response.encoding or 'ISO-8859-1'
|
||||
original = response.raw._original_response
|
||||
status_line, headers, body = (
|
||||
u'HTTP/{version} {status} {reason}'.format(
|
||||
version='.'.join(str(original.version)),
|
||||
status=original.status, reason=original.reason,
|
||||
),
|
||||
str(original.msg).decode(encoding),
|
||||
response.content.decode(encoding) if response.content else u''
|
||||
)
|
||||
|
||||
if args.prettify and sys.stdout.isatty():
|
||||
if args.print_headers:
|
||||
status_line = pretty.prettify_http(status_line).strip()
|
||||
headers = pretty.prettify_http(headers)
|
||||
if args.print_body:
|
||||
body = pretty.prettify_body(body,
|
||||
response.headers['content-type'])
|
||||
|
||||
if args.print_headers:
|
||||
print status_line
|
||||
print headers
|
||||
if args.print_body:
|
||||
print body
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
79
httpie/httpmessage.py
Normal file
79
httpie/httpmessage.py
Normal file
@ -0,0 +1,79 @@
|
||||
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
|
||||
|
||||
if isinstance(body, dict):
|
||||
# --form
|
||||
body = request.__class__._encode_params(body)
|
||||
|
||||
return HTTPMessage(
|
||||
line='{method} {path}{query} HTTP/1.1'.format(
|
||||
method=request.method,
|
||||
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()),
|
||||
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)
|
@ -1,55 +1,50 @@
|
||||
import os
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
import pygments
|
||||
from pygments.lexers import get_lexer_for_mimetype
|
||||
|
||||
from pygments.util import ClassNotFound
|
||||
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.lexer import RegexLexer, bygroups
|
||||
from pygments import token
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
|
||||
from . import solarized
|
||||
|
||||
|
||||
TYPE_JS = 'application/javascript'
|
||||
DEFAULT_STYLE = 'solarized'
|
||||
AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys())
|
||||
FORMATTER = (Terminal256Formatter
|
||||
if '256color' in os.environ.get('TERM', '')
|
||||
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):
|
||||
|
||||
def __init__(self, style_name):
|
||||
if style_name == 'solarized':
|
||||
style = solarized.SolarizedStyle
|
||||
else:
|
||||
style = get_style_by_name(style_name)
|
||||
self.formatter = FORMATTER(style=style)
|
||||
|
||||
highlight = partial(pygments.highlight,
|
||||
formatter=Terminal256Formatter(
|
||||
style=solarized.SolarizedStyle))
|
||||
highlight_http = partial(highlight, lexer=HTTPLexer())
|
||||
def headers(self, content):
|
||||
return pygments.highlight(content, HttpLexer(), self.formatter)
|
||||
|
||||
|
||||
def prettify_http(headers):
|
||||
return highlight_http(headers)
|
||||
|
||||
|
||||
def prettify_body(content, content_type):
|
||||
content_type = content_type.split(';')[0]
|
||||
if 'json' in content_type:
|
||||
content_type = TYPE_JS
|
||||
def body(self, content, content_type):
|
||||
content_type = content_type.split(';')[0]
|
||||
try:
|
||||
# Indent JSON
|
||||
content = json.dumps(json.loads(content),
|
||||
sort_keys=True, indent=4)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
lexer = get_lexer_for_mimetype(content_type)
|
||||
content = highlight(code=content, lexer=lexer)
|
||||
if content:
|
||||
content = content[:-1]
|
||||
except Exception:
|
||||
pass
|
||||
return content
|
||||
lexer = get_lexer_for_mimetype(content_type)
|
||||
except ClassNotFound:
|
||||
return content
|
||||
|
||||
if content_type == 'application/json':
|
||||
try:
|
||||
# Indent and sort the JSON data.
|
||||
content = json.dumps(json.loads(content),
|
||||
sort_keys=True, indent=4,
|
||||
ensure_ascii=False)
|
||||
except:
|
||||
pass
|
||||
|
||||
return pygments.highlight(content, lexer, self.formatter)
|
||||
|
@ -27,7 +27,8 @@ THE SOFTWARE.
|
||||
|
||||
"""
|
||||
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'
|
||||
|
51
setup.py
51
setup.py
@ -1,12 +1,55 @@
|
||||
import os
|
||||
import sys
|
||||
from setuptools import setup
|
||||
import httpie
|
||||
|
||||
|
||||
setup(name='httpie',version=httpie.__version__,
|
||||
if sys.argv[-1] == 'test':
|
||||
sys.exit(os.system('python tests/tests.py'))
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
setup(
|
||||
name='httpie',
|
||||
version=httpie.__version__,
|
||||
description=httpie.__doc__.strip(),
|
||||
url='https://github.com/jkbr/httpie',
|
||||
long_description=open('README.rst').read(),
|
||||
url='http://httpie.org/',
|
||||
download_url='https://github.com/jkbr/httpie',
|
||||
author=httpie.__author__,
|
||||
author_email='jakub@roztocil.name',
|
||||
license=httpie.__licence__,
|
||||
packages=['httpie'],
|
||||
entry_points={'console_scripts': ['http = httpie.httpie:main']},
|
||||
install_requires=['requests>=0.10.4', 'Pygments>=1.4'])
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'http = httpie.__main__:main',
|
||||
],
|
||||
},
|
||||
install_requires=requirements,
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.1',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Software Development',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: Terminals',
|
||||
'Topic :: Text Processing',
|
||||
],
|
||||
)
|
||||
|
1
tests/file.txt
Normal file
1
tests/file.txt
Normal file
@ -0,0 +1 @@
|
||||
__test_file_content__
|
1
tests/file2.txt
Normal file
1
tests/file2.txt
Normal file
@ -0,0 +1 @@
|
||||
__test_file_content__
|
495
tests/tests.py
Normal file
495
tests/tests.py
Normal file
@ -0,0 +1,495 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
import argparse
|
||||
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__, cliparse
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
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 BaseTestCase(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)
|
||||
|
||||
|
||||
#################################################################
|
||||
# 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(
|
||||
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_PATH)
|
||||
])
|
||||
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_PATH),
|
||||
])
|
||||
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 ArgumentParserTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.parser = cliparse.Parser()
|
||||
|
||||
def test_guess_when_method_set_and_valid(self):
|
||||
args = argparse.Namespace()
|
||||
args.method = 'GET'
|
||||
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_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 FakeResponse(Response):
|
||||
|
||||
class Mock(object):
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return u'Mock string'
|
||||
|
||||
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 UnicodeOutputTestCase(BaseTestCase):
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
for key, value in response_dict.iteritems():
|
||||
self.assertIn(key, output)
|
||||
self.assertIn(value, output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
19
tox.ini
Normal file
19
tox.ini
Normal 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
|
Reference in New Issue
Block a user