mirror of
https://github.com/httpie/cli.git
synced 2025-08-11 00:13:36 +02:00
Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -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
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
|
107
README.md
107
README.md
@ -1,107 +0,0 @@
|
|||||||
## HTTPie: cURL for humans
|
|
||||||
|
|
||||||
[](http://travis-ci.org/jkbr/httpie)
|
|
||||||
|
|
||||||
|
|
||||||
HTTPie is a CLI frontend for [python-requests](http://python-requests.org) built out of frustration. It provides an `http` command that can be used to easily issue HTTP requests. It is meant to be used by humans to interact with HTTP-based APIs and web servers. The response headers are colorized and the body is syntax-highlighted if its `Content-Type` is known to [Pygments](http://pygments.org/) (unless the output is redirected).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
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
205
README.rst
Normal 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)
|
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'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.1.6'
|
__version__ = '0.2.1'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
@ -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__':
|
||||||
|
196
httpie/cli.py
196
httpie/cli.py
@ -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
169
httpie/cliparse.py
Normal 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
74
httpie/httpmessage.py
Normal 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)
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
23
setup.py
23
setup.py
@ -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
115
tests.py
@ -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
1
tests/file.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
__test_file_content__
|
193
tests/tests.py
Normal file
193
tests/tests.py
Normal 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()
|
Reference in New Issue
Block a user