mirror of
https://github.com/httpie/cli.git
synced 2025-08-13 15:07:54 +02:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
43cc3e7ddb | |||
f1224da526 | |||
e0cc63c7eb | |||
52dd6adaa3 | |||
1aa77017d5 | |||
748a0a480d | |||
01df344a07 | |||
b1074ccb4f | |||
7a84163d1c | |||
a31d552d1c | |||
5a037b2e13 | |||
6af42b1827 | |||
0e267d8efa | |||
927acc283e | |||
817165f5ff | |||
4fe3deb9d9 | |||
9034546b80 | |||
2c12fd99f9 | |||
70eb97dece | |||
8a52bef559 | |||
711168a899 | |||
81c99886fd | |||
2e535d8345 | |||
0bcd4d2fb0 | |||
d5bc564e4f |
@ -29,3 +29,4 @@ Patches and ideas
|
|||||||
* `Davey Shafik <https://github.com/dshafik>`_
|
* `Davey Shafik <https://github.com/dshafik>`_
|
||||||
* `cido <https://github.com/cido>`_
|
* `cido <https://github.com/cido>`_
|
||||||
* `Justin Bonnar <https://github.com/jargonjustin>`_
|
* `Justin Bonnar <https://github.com/jargonjustin>`_
|
||||||
|
* `Nathan LaFreniere <https://github.com/nlf>`_
|
||||||
|
112
README.rst
112
README.rst
@ -121,7 +121,6 @@ See also ``http --help``.
|
|||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
||||||
Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data:
|
Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
@ -226,6 +225,42 @@ The only information HTTPie needs to perform a request is a URL.
|
|||||||
The default scheme is, somewhat unsurprisingly, ``http://``,
|
The default scheme is, somewhat unsurprisingly, ``http://``,
|
||||||
and can be omitted from the argument – ``http example.org`` works just fine.
|
and can be omitted from the argument – ``http example.org`` works just fine.
|
||||||
|
|
||||||
|
Additionally, curl-like shorthand for localhost is supported.
|
||||||
|
This means that, for example ``:3000`` would expand to ``http://localhost:3000``
|
||||||
|
If the port is omitted, then port 80 is assumed.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http :/foo
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: http
|
||||||
|
|
||||||
|
GET /foo HTTP/1.1
|
||||||
|
Host: localhost
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http :3000/bar
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: http
|
||||||
|
|
||||||
|
GET /bar HTTP/1.1
|
||||||
|
Host: localhost:3000
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http :
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: http
|
||||||
|
|
||||||
|
GET / HTTP/1.1
|
||||||
|
Host: localhost
|
||||||
|
|
||||||
If find yourself manually constructing URLs with **querystring parameters**
|
If find yourself manually constructing URLs with **querystring parameters**
|
||||||
on the terminal, you may appreciate the ``param==value`` syntax for appending
|
on the terminal, you may appreciate the ``param==value`` syntax for appending
|
||||||
URL parameters so that you don't have to worry about escaping the ``&``
|
URL parameters so that you don't have to worry about escaping the ``&``
|
||||||
@ -246,14 +281,15 @@ command:
|
|||||||
Request Items
|
Request Items
|
||||||
=============
|
=============
|
||||||
|
|
||||||
There are five different *request item* types that provide a
|
There are a few different *request item* types that provide a
|
||||||
convenient mechanism for specifying HTTP headers, simple JSON and
|
convenient mechanism for specifying HTTP headers, simple JSON and
|
||||||
form data, files, and URL parameters.
|
form data, files, and URL parameters.
|
||||||
|
|
||||||
They are key/value pairs specified after the URL. All have in
|
They are key/value pairs specified after the URL. All have in
|
||||||
common that they become part of the actual request that is sent and that
|
common that they become part of the actual request that is sent and that
|
||||||
their type is distinguished only by the separator used:
|
their type is distinguished only by the separator used:
|
||||||
``:``, ``=``, ``:=``, ``@``, and ``==``.
|
``:``, ``=``, ``:=``, ``==``, ``@``, ``=@``, and ``:=@``. The ones with an
|
||||||
|
``@`` expect a file path as value.
|
||||||
|
|
||||||
+-----------------------+-----------------------------------------------------+
|
+-----------------------+-----------------------------------------------------+
|
||||||
| Item Type | Description |
|
| Item Type | Description |
|
||||||
@ -266,16 +302,16 @@ their type is distinguished only by the separator used:
|
|||||||
| | The ``==`` separator is used |
|
| | The ``==`` separator is used |
|
||||||
+-----------------------+-----------------------------------------------------+
|
+-----------------------+-----------------------------------------------------+
|
||||||
| Data Fields | Request data fields to be serialized as a JSON |
|
| Data Fields | Request data fields to be serialized as a JSON |
|
||||||
| ``field=value`` | object (default), or to be form encoded |
|
| ``field=value``, | object (default), or to be form-encoded |
|
||||||
| | (``--form, -f``). |
|
| ``field=@file.txt`` | (``--form, -f``). |
|
||||||
+-----------------------+-----------------------------------------------------+
|
+-----------------------+-----------------------------------------------------+
|
||||||
| Raw JSON fields | Useful when sending JSON and one or |
|
| Raw JSON fields | Useful when sending JSON and one or |
|
||||||
| ``field:=json`` | more fields need to be a ``Boolean``, ``Number``, |
|
| ``field:=json``, | more fields need to be a ``Boolean``, ``Number``, |
|
||||||
| | nested ``Object``, or an ``Array``, e.g., |
|
| ``field:=@file.json`` | nested ``Object``, or an ``Array``, e.g., |
|
||||||
| | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` |
|
| | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` |
|
||||||
| | (note the quotes). |
|
| | (note the quotes). |
|
||||||
+-----------------------+-----------------------------------------------------+
|
+-----------------------+-----------------------------------------------------+
|
||||||
| Files | Only available with ``--form, -f``. |
|
| Form File Fields | Only available with ``--form, -f``. |
|
||||||
| ``field@/dir/file`` | For example ``screenshot@~/Pictures/img.png``. |
|
| ``field@/dir/file`` | For example ``screenshot@~/Pictures/img.png``. |
|
||||||
| | The presence of a file field results |
|
| | The presence of a file field results |
|
||||||
| | in a ``multipart/form-data`` request. |
|
| | in a ``multipart/form-data`` request. |
|
||||||
@ -285,6 +321,8 @@ You can use ``\`` to escape characters that shouldn't be used as separators
|
|||||||
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value
|
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value
|
||||||
pair (``foo=`` and ``bar``) instead of a URL parameter.
|
pair (``foo=`` and ``bar``) instead of a URL parameter.
|
||||||
|
|
||||||
|
You can also quote values, e.g. ``foo="bar baz"``.
|
||||||
|
|
||||||
Note that data fields aren't the only way to specify request data:
|
Note that data fields aren't the only way to specify request data:
|
||||||
`Redirected input`_ allows for passing arbitrary data to be sent with the
|
`Redirected input`_ allows for passing arbitrary data to be sent with the
|
||||||
request.
|
request.
|
||||||
@ -332,11 +370,16 @@ Simple example:
|
|||||||
|
|
||||||
|
|
||||||
Non-string fields use the ``:=`` separator, which allows you to embed raw JSON
|
Non-string fields use the ``:=`` separator, which allows you to embed raw JSON
|
||||||
into the resulting object:
|
into the resulting object. Text and raw JSON files can also be embedded into
|
||||||
|
fields using ``=@`` and ``:=@``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http PUT api.example.com/person/1 name=John age:=29 married:=false hobbies:='["http", "pies"]'
|
$ http PUT api.example.com/person/1 \
|
||||||
|
name=John \
|
||||||
|
age:=29 married:=false hobbies:='["http", "pies"]' \ # Raw JSON
|
||||||
|
description=@about-john.txt \ # Embed text file
|
||||||
|
bookmarks:=@bookmarks.json # Embed JSON file
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: http
|
.. code-block:: http
|
||||||
@ -352,8 +395,12 @@ into the resulting object:
|
|||||||
"http",
|
"http",
|
||||||
"pies"
|
"pies"
|
||||||
],
|
],
|
||||||
|
"description": "John is a nice guy who likes pies.",
|
||||||
"married": false,
|
"married": false,
|
||||||
"name": "John"
|
"name": "John",
|
||||||
|
"bookmarks": {
|
||||||
|
"HTTPie": "http://httpie.org",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -383,7 +430,7 @@ Regular Forms
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http --form POST api.example.org/person/1 name='John Smith' email=john@example.org
|
$ http --form POST api.example.org/person/1 name='John Smith' email=john@example.org cv=@~/Documents/cv.txt
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: http
|
.. code-block:: http
|
||||||
@ -391,7 +438,7 @@ Regular Forms
|
|||||||
POST /person/1 HTTP/1.1
|
POST /person/1 HTTP/1.1
|
||||||
Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
||||||
|
|
||||||
name=John+Smith&email=john%40example.org
|
name=John+Smith&email=john%40example.org&cv=John's+CV+...
|
||||||
|
|
||||||
|
|
||||||
-----------------
|
-----------------
|
||||||
@ -416,6 +463,9 @@ submitted:
|
|||||||
<input type="file" name="cv" />
|
<input type="file" name="cv" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
Note that ``@`` is used to simulate a file upload form field, whereas
|
||||||
|
``=@`` just embeds the file content as a regular text field value.
|
||||||
|
|
||||||
|
|
||||||
============
|
============
|
||||||
HTTP Headers
|
HTTP Headers
|
||||||
@ -519,8 +569,9 @@ Authorization information from your ``~/.netrc`` file is honored as well:
|
|||||||
Auth Plugins
|
Auth Plugins
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* `httpie-ntlm <https://github.com/jkbr/httpie-ntlm>`_
|
* `httpie-oauth <https://github.com/jkbr/httpie-oauth>`_: OAuth
|
||||||
* `httpie-oauth <https://github.com/jkbr/httpie-oauth>`_
|
* `httpie-ntlm <https://github.com/jkbr/httpie-ntlm>`_: NTLM (NT LAN Manager)
|
||||||
|
* `httpie-negotiate <https://github.com/ndzou/httpie-negotiate>`_: SPNEGO (GSS Negotiate)
|
||||||
|
|
||||||
|
|
||||||
=======
|
=======
|
||||||
@ -532,7 +583,7 @@ protocol (which is included in the value in case of redirects across protocols):
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http --proxy=http:10.10.1.10:3128 --proxy=https:10.10.1.10:1080 example.org
|
$ http --proxy=http:http://10.10.1.10:3128 --proxy=https:https://10.10.1.10:1080 example.org
|
||||||
|
|
||||||
|
|
||||||
With Basic authentication:
|
With Basic authentication:
|
||||||
@ -550,8 +601,8 @@ In your ``~/.bash_profile``:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
export HTTP_PROXY=10.10.1.10:3128
|
export HTTP_PROXY=http://10.10.1.10:3128
|
||||||
export HTTPS_PROXY=10.10.1.10:1080
|
export HTTPS_PROXY=https://10.10.1.10:1080
|
||||||
export NO_PROXY=localhost,example.com
|
export NO_PROXY=localhost,example.com
|
||||||
|
|
||||||
|
|
||||||
@ -1197,7 +1248,7 @@ See `claudiatd/httpie-artwork`_
|
|||||||
Authors
|
Authors
|
||||||
=======
|
=======
|
||||||
|
|
||||||
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_
|
`Jakub Roztocil`_ (`@jkbrzt`_) created HTTPie and `these fine people`_
|
||||||
have contributed.
|
have contributed.
|
||||||
|
|
||||||
=======
|
=======
|
||||||
@ -1213,7 +1264,13 @@ Changelog
|
|||||||
|
|
||||||
*You can click a version name to see a diff with the previous one.*
|
*You can click a version name to see a diff with the previous one.*
|
||||||
|
|
||||||
* `0.8.0-dev`_
|
* `0.9.0-dev`_
|
||||||
|
* `0.8.0`_ (2014-01-25)
|
||||||
|
* Added ``field=@file.txt`` and ``field:=@file.json`` for embedding
|
||||||
|
the contents of text and JSON files into request data.
|
||||||
|
* Added curl-style shorthand for localhost.
|
||||||
|
* Fixed request ``Host`` header value output so that it doesn't contain
|
||||||
|
credentials, if included in the URL.
|
||||||
* `0.7.1`_ (2013-09-24)
|
* `0.7.1`_ (2013-09-24)
|
||||||
* Added ``--ignore-stdin``.
|
* Added ``--ignore-stdin``.
|
||||||
* Added support for auth plugins.
|
* Added support for auth plugins.
|
||||||
@ -1311,6 +1368,14 @@ Changelog
|
|||||||
* `0.1.6`_ (2012-03-04)
|
* `0.1.6`_ (2012-03-04)
|
||||||
|
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://d2weczhvl823v0.cloudfront.net/jkbr/httpie/trend.png
|
||||||
|
:target: https://bitdeli.com/free
|
||||||
|
:alt: Bitdeli Badge
|
||||||
|
|
||||||
|
|
||||||
.. _Requests: http://python-requests.org
|
.. _Requests: http://python-requests.org
|
||||||
.. _Pygments: http://pygments.org/
|
.. _Pygments: http://pygments.org/
|
||||||
.. _pip: http://www.pip-installer.org/en/latest/index.html
|
.. _pip: http://www.pip-installer.org/en/latest/index.html
|
||||||
@ -1321,8 +1386,8 @@ Changelog
|
|||||||
.. _Debian: http://packages.debian.org/httpie
|
.. _Debian: http://packages.debian.org/httpie
|
||||||
.. _the repository: https://github.com/jkbr/httpie
|
.. _the repository: https://github.com/jkbr/httpie
|
||||||
.. _these fine people: https://github.com/jkbr/httpie/contributors
|
.. _these fine people: https://github.com/jkbr/httpie/contributors
|
||||||
.. _Jakub Roztocil: http://roztocil.name
|
.. _Jakub Roztocil: http://subtleapps.com
|
||||||
.. _@jakubroztocil: https://twitter.com/jakubroztocil
|
.. _@jkbrzt: https://twitter.com/jkbrzt
|
||||||
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
|
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
|
||||||
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
|
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
|
||||||
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
|
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
|
||||||
@ -1339,6 +1404,7 @@ Changelog
|
|||||||
.. _0.5.1: https://github.com/jkbr/httpie/compare/0.5.0...0.5.1
|
.. _0.5.1: https://github.com/jkbr/httpie/compare/0.5.0...0.5.1
|
||||||
.. _0.6.0: https://github.com/jkbr/httpie/compare/0.5.1...0.6.0
|
.. _0.6.0: https://github.com/jkbr/httpie/compare/0.5.1...0.6.0
|
||||||
.. _0.7.1: https://github.com/jkbr/httpie/compare/0.6.0...0.7.1
|
.. _0.7.1: https://github.com/jkbr/httpie/compare/0.6.0...0.7.1
|
||||||
.. _0.8.0-dev: https://github.com/jkbr/httpie/compare/0.7.1...master
|
.. _0.8.0: https://github.com/jkbr/httpie/compare/0.7.1...0.8.0
|
||||||
|
.. _0.9.0-dev: https://github.com/jkbr/httpie/compare/0.8.0...master
|
||||||
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
|
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
|
||||||
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE
|
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE
|
||||||
|
@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.7.1'
|
__version__ = '0.8.0'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from .plugins import plugin_manager
|
|||||||
from .sessions import DEFAULT_SESSIONS_DIR
|
from .sessions import DEFAULT_SESSIONS_DIR
|
||||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
||||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS,
|
||||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||||
OUT_RESP_BODY, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
OUT_RESP_BODY, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
|
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
|
||||||
@ -88,13 +88,18 @@ positional.add_argument(
|
|||||||
help="""
|
help="""
|
||||||
The scheme defaults to 'http://' if the URL does not include one.
|
The scheme defaults to 'http://' if the URL does not include one.
|
||||||
|
|
||||||
|
You can also use a shorthand for localhost
|
||||||
|
|
||||||
|
$ http :3000 # => http://localhost:3000
|
||||||
|
$ http :/foo # => http://localhost/foo
|
||||||
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
positional.add_argument(
|
positional.add_argument(
|
||||||
'items',
|
'items',
|
||||||
metavar='REQUEST ITEM',
|
metavar='REQUEST_ITEM',
|
||||||
nargs=ZERO_OR_MORE,
|
nargs=ZERO_OR_MORE,
|
||||||
type=KeyValueArgType(*SEP_GROUP_ITEMS),
|
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
|
||||||
help=r"""
|
help=r"""
|
||||||
Optional key-value pairs to be included in the request. The separator used
|
Optional key-value pairs to be included in the request. The separator used
|
||||||
determines the type:
|
determines the type:
|
||||||
@ -112,13 +117,21 @@ positional.add_argument(
|
|||||||
|
|
||||||
name=HTTPie language=Python description='CLI HTTP client'
|
name=HTTPie language=Python description='CLI HTTP client'
|
||||||
|
|
||||||
|
':=' Non-string JSON data fields (only with --json, -j):
|
||||||
|
|
||||||
|
awesome:=true amount:=42 colors:='["red", "green", "blue"]'
|
||||||
|
|
||||||
'@' Form file fields (only with --form, -f):
|
'@' Form file fields (only with --form, -f):
|
||||||
|
|
||||||
cs@~/Documents/CV.pdf
|
cs@~/Documents/CV.pdf
|
||||||
|
|
||||||
':=' Non-string JSON data fields (only with --json, -j):
|
'=@' A data field like '=', but takes a file path and embeds its content:
|
||||||
|
|
||||||
awesome:=true amount:=42 colors:='["red", "green", "blue"]'
|
essay=@Documents/essay.txt
|
||||||
|
|
||||||
|
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
|
||||||
|
|
||||||
|
package:=@./package.json
|
||||||
|
|
||||||
You can use a backslash to escape a colliding separator in the field name:
|
You can use a backslash to escape a colliding separator in the field name:
|
||||||
|
|
||||||
@ -186,7 +199,7 @@ output_processing.add_argument(
|
|||||||
default=DEFAULT_STYLE,
|
default=DEFAULT_STYLE,
|
||||||
choices=AVAILABLE_STYLES,
|
choices=AVAILABLE_STYLES,
|
||||||
help="""
|
help="""
|
||||||
Output coloring style (default is "{default}"). On of:
|
Output coloring style (default is "{default}"). One of:
|
||||||
|
|
||||||
{available}
|
{available}
|
||||||
|
|
||||||
@ -422,11 +435,12 @@ network.add_argument(
|
|||||||
'--proxy',
|
'--proxy',
|
||||||
default=[],
|
default=[],
|
||||||
action='append',
|
action='append',
|
||||||
metavar='PROTOCOL:HOST',
|
metavar='PROTOCOL:PROXY_URL',
|
||||||
type=KeyValueArgType(SEP_PROXY),
|
type=KeyValueArgType(SEP_PROXY),
|
||||||
help="""
|
help="""
|
||||||
String mapping protocol to the URL of the proxy (e.g. http:foo.bar:3128).
|
String mapping protocol to the URL of the proxy
|
||||||
You can specify multiple proxies with different protocols.
|
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||||
|
different protocols.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ import sys
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from getpass import getpass
|
import getpass
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
#noinspection PyCompatibility
|
#noinspection PyCompatibility
|
||||||
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
||||||
@ -37,22 +37,40 @@ SEP_PROXY = ':'
|
|||||||
SEP_DATA = '='
|
SEP_DATA = '='
|
||||||
SEP_DATA_RAW_JSON = ':='
|
SEP_DATA_RAW_JSON = ':='
|
||||||
SEP_FILES = '@'
|
SEP_FILES = '@'
|
||||||
|
SEP_DATA_EMBED_FILE = '=@'
|
||||||
|
SEP_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||||
SEP_QUERY = '=='
|
SEP_QUERY = '=='
|
||||||
|
|
||||||
# Separators that become request data
|
# Separators that become request data
|
||||||
SEP_GROUP_DATA_ITEMS = frozenset([
|
SEP_GROUP_DATA_ITEMS = frozenset([
|
||||||
SEP_DATA,
|
SEP_DATA,
|
||||||
SEP_DATA_RAW_JSON,
|
SEP_DATA_RAW_JSON,
|
||||||
SEP_FILES
|
SEP_FILES,
|
||||||
|
SEP_DATA_EMBED_FILE,
|
||||||
|
SEP_DATA_EMBED_RAW_JSON_FILE
|
||||||
|
])
|
||||||
|
|
||||||
|
# Separators for items whose value is a filename to be embedded
|
||||||
|
SEP_GROUP_DATA_EMBED_ITEMS = frozenset([
|
||||||
|
SEP_DATA_EMBED_FILE,
|
||||||
|
SEP_DATA_EMBED_RAW_JSON_FILE,
|
||||||
|
])
|
||||||
|
|
||||||
|
# Separators for raw JSON items
|
||||||
|
SEP_GROUP_RAW_JSON_ITEMS = frozenset([
|
||||||
|
SEP_DATA_RAW_JSON,
|
||||||
|
SEP_DATA_EMBED_RAW_JSON_FILE,
|
||||||
])
|
])
|
||||||
|
|
||||||
# Separators allowed in ITEM arguments
|
# Separators allowed in ITEM arguments
|
||||||
SEP_GROUP_ITEMS = frozenset([
|
SEP_GROUP_ALL_ITEMS = frozenset([
|
||||||
SEP_HEADERS,
|
SEP_HEADERS,
|
||||||
SEP_QUERY,
|
SEP_QUERY,
|
||||||
SEP_DATA,
|
SEP_DATA,
|
||||||
SEP_DATA_RAW_JSON,
|
SEP_DATA_RAW_JSON,
|
||||||
SEP_FILES
|
SEP_FILES,
|
||||||
|
SEP_DATA_EMBED_FILE,
|
||||||
|
SEP_DATA_EMBED_RAW_JSON_FILE,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@ -120,7 +138,18 @@ class Parser(ArgumentParser):
|
|||||||
if not (self.args.url.startswith((HTTP, HTTPS))):
|
if not (self.args.url.startswith((HTTP, HTTPS))):
|
||||||
# Default to 'https://' if invoked as `https args`.
|
# Default to 'https://' if invoked as `https args`.
|
||||||
scheme = HTTPS if self.env.progname == 'https' else HTTP
|
scheme = HTTPS if self.env.progname == 'https' else HTTP
|
||||||
self.args.url = scheme + self.args.url
|
|
||||||
|
# See if we're using curl style shorthand for localhost (:3000/foo)
|
||||||
|
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
|
||||||
|
if shorthand:
|
||||||
|
port = shorthand.group(1)
|
||||||
|
rest = shorthand.group(2)
|
||||||
|
self.args.url = scheme + 'localhost'
|
||||||
|
if port:
|
||||||
|
self.args.url += ':' + port
|
||||||
|
self.args.url += rest
|
||||||
|
else:
|
||||||
|
self.args.url = scheme + self.args.url
|
||||||
self._process_auth()
|
self._process_auth()
|
||||||
|
|
||||||
return self.args
|
return self.args
|
||||||
@ -257,7 +286,7 @@ class Parser(ArgumentParser):
|
|||||||
# Parse the URL as an ITEM and store it as the first ITEM arg.
|
# Parse the URL as an ITEM and store it as the first ITEM arg.
|
||||||
self.args.items.insert(
|
self.args.items.insert(
|
||||||
0,
|
0,
|
||||||
KeyValueArgType(*SEP_GROUP_ITEMS).__call__(self.args.url)
|
KeyValueArgType(*SEP_GROUP_ALL_ITEMS).__call__(self.args.url)
|
||||||
)
|
)
|
||||||
|
|
||||||
except ArgumentTypeError as e:
|
except ArgumentTypeError as e:
|
||||||
@ -558,9 +587,7 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
|
|||||||
params = ParamDict()
|
params = ParamDict()
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
value = item.value
|
value = item.value
|
||||||
key = item.key
|
|
||||||
|
|
||||||
if item.sep == SEP_HEADERS:
|
if item.sep == SEP_HEADERS:
|
||||||
target = headers
|
target = headers
|
||||||
@ -572,21 +599,34 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
|
|||||||
value = (os.path.basename(value),
|
value = (os.path.basename(value),
|
||||||
BytesIO(f.read()))
|
BytesIO(f.read()))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise ParseError(
|
raise ParseError('"%s": %s' % (item.orig, e))
|
||||||
'Invalid argument "%s": %s' % (item.orig, e))
|
|
||||||
target = files
|
target = files
|
||||||
|
|
||||||
elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]:
|
elif item.sep in SEP_GROUP_DATA_ITEMS:
|
||||||
if item.sep == SEP_DATA_RAW_JSON:
|
|
||||||
|
if item.sep in SEP_GROUP_DATA_EMBED_ITEMS:
|
||||||
try:
|
try:
|
||||||
value = json.loads(item.value)
|
with open(os.path.expanduser(value), 'rb') as f:
|
||||||
except ValueError:
|
value = f.read().decode('utf8')
|
||||||
raise ParseError('"%s" is not valid JSON' % item.orig)
|
except IOError as e:
|
||||||
|
raise ParseError('"%s": %s' % (item.orig, e))
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise ParseError(
|
||||||
|
'"%s": cannot embed the content of "%s",'
|
||||||
|
' not a UTF8 or ASCII-encoded text file'
|
||||||
|
% (item.orig, item.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.sep in SEP_GROUP_RAW_JSON_ITEMS:
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ParseError('"%s": %s' % (item.orig, e))
|
||||||
target = data
|
target = data
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise TypeError(item)
|
raise TypeError(item)
|
||||||
|
|
||||||
target[key] = value
|
target[item.key] = value
|
||||||
|
|
||||||
return headers, data, files, params
|
return headers, data, files, params
|
||||||
|
@ -155,7 +155,7 @@ class HTTPRequest(HTTPMessage):
|
|||||||
headers = dict(self._orig.headers)
|
headers = dict(self._orig.headers)
|
||||||
|
|
||||||
if 'Host' not in headers:
|
if 'Host' not in headers:
|
||||||
headers['Host'] = url.netloc
|
headers['Host'] = url.netloc.split('@')[-1]
|
||||||
|
|
||||||
headers = ['%s: %s' % (name, value)
|
headers = ['%s: %s' % (name, value)
|
||||||
for name, value in headers.items()]
|
for name, value in headers.items()]
|
||||||
|
3
tests/fixtures/test.json
vendored
Normal file
3
tests/fixtures/test.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"hello": "world"
|
||||||
|
}
|
@ -64,6 +64,7 @@ sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
|||||||
|
|
||||||
from httpie import ExitStatus
|
from httpie import ExitStatus
|
||||||
from httpie import input
|
from httpie import input
|
||||||
|
from httpie.cli import parser
|
||||||
from httpie.models import Environment
|
from httpie.models import Environment
|
||||||
from httpie.core import main
|
from httpie.core import main
|
||||||
from httpie.output import BINARY_SUPPRESSED_NOTICE
|
from httpie.output import BINARY_SUPPRESSED_NOTICE
|
||||||
@ -101,15 +102,22 @@ def patharg(path):
|
|||||||
FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.txt')
|
FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.txt')
|
||||||
FILE2_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file2.txt')
|
FILE2_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file2.txt')
|
||||||
BIN_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.bin')
|
BIN_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.bin')
|
||||||
|
JSON_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'test.json')
|
||||||
|
|
||||||
FILE_PATH_ARG = patharg(FILE_PATH)
|
FILE_PATH_ARG = patharg(FILE_PATH)
|
||||||
FILE2_PATH_ARG = patharg(FILE2_PATH)
|
FILE2_PATH_ARG = patharg(FILE2_PATH)
|
||||||
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
|
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
|
||||||
|
JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
|
||||||
|
|
||||||
with open(FILE_PATH) as f:
|
with open(FILE_PATH) as f:
|
||||||
|
# Strip because we don't want new lines in the data so that we can
|
||||||
|
# easily count occurrences also when embedded in JSON (where the new
|
||||||
|
# line would be escaped).
|
||||||
FILE_CONTENT = f.read().strip()
|
FILE_CONTENT = f.read().strip()
|
||||||
with open(BIN_FILE_PATH, 'rb') as f:
|
with open(BIN_FILE_PATH, 'rb') as f:
|
||||||
BIN_FILE_CONTENT = f.read()
|
BIN_FILE_CONTENT = f.read()
|
||||||
|
with open(JSON_FILE_PATH, 'rb') as f:
|
||||||
|
JSON_FILE_CONTENT = f.read()
|
||||||
|
|
||||||
|
|
||||||
def httpbin(path):
|
def httpbin(path):
|
||||||
@ -1172,11 +1180,7 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.key_value_type = input.KeyValueArgType(
|
self.key_value_type = input.KeyValueArgType(
|
||||||
input.SEP_HEADERS,
|
*input.SEP_GROUP_ALL_ITEMS
|
||||||
input.SEP_QUERY,
|
|
||||||
input.SEP_DATA,
|
|
||||||
input.SEP_DATA_RAW_JSON,
|
|
||||||
input.SEP_FILES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_items(self):
|
def test_invalid_items(self):
|
||||||
@ -1223,26 +1227,99 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
self.key_value_type('eh:'),
|
self.key_value_type('eh:'),
|
||||||
self.key_value_type('ed='),
|
self.key_value_type('ed='),
|
||||||
self.key_value_type('bool:=true'),
|
self.key_value_type('bool:=true'),
|
||||||
self.key_value_type('test-file@%s' % FILE_PATH_ARG),
|
self.key_value_type('file@' + FILE_PATH_ARG),
|
||||||
self.key_value_type('query==value'),
|
self.key_value_type('query==value'),
|
||||||
|
self.key_value_type('string-embed=@' + FILE_PATH_ARG),
|
||||||
|
self.key_value_type('raw-json-embed:=@' + JSON_FILE_PATH_ARG),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Parsed headers
|
||||||
# `requests.structures.CaseInsensitiveDict` => `dict`
|
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||||
headers = dict(headers._store.values())
|
headers = dict(headers._store.values())
|
||||||
self.assertDictEqual(headers, {
|
self.assertDictEqual(headers, {
|
||||||
'header': 'value',
|
'header': 'value',
|
||||||
'eh': ''
|
'eh': ''
|
||||||
})
|
})
|
||||||
self.assertDictEqual(data, {
|
|
||||||
|
# Parsed data
|
||||||
|
raw_json_embed = data.pop('raw-json-embed')
|
||||||
|
self.assertDictEqual(raw_json_embed, json.loads(
|
||||||
|
JSON_FILE_CONTENT.decode('utf8')))
|
||||||
|
data['string-embed'] = data['string-embed'].strip()
|
||||||
|
self.assertDictEqual(dict(data), {
|
||||||
"ed": "",
|
"ed": "",
|
||||||
"string": "value",
|
"string": "value",
|
||||||
"bool": True,
|
"bool": True,
|
||||||
"list": ["a", 1, {}, False],
|
"list": ["a", 1, {}, False],
|
||||||
"obj": {"a": "b"},
|
"obj": {"a": "b"},
|
||||||
|
"string-embed": FILE_CONTENT,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Parsed query string parameters
|
||||||
self.assertDictEqual(params, {
|
self.assertDictEqual(params, {
|
||||||
'query': 'value',
|
'query': 'value',
|
||||||
})
|
})
|
||||||
self.assertIn('test-file', files)
|
|
||||||
|
# Parsed file fields
|
||||||
|
self.assertIn('file', files)
|
||||||
|
self.assertEqual(files['file'][1].read().strip().decode('utf8'),
|
||||||
|
FILE_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class CLIParserTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_expand_localhost_shorthand(self):
|
||||||
|
args = parser.parse_args(args=[':'], env=TestEnvironment())
|
||||||
|
|
||||||
|
self.assertEqual(args.url, 'http://localhost')
|
||||||
|
|
||||||
|
def test_expand_localhost_shorthand_with_slash(self):
|
||||||
|
args = parser.parse_args(args=[':/'], env=TestEnvironment())
|
||||||
|
|
||||||
|
self.assertEqual(args.url, 'http://localhost/')
|
||||||
|
|
||||||
|
def test_expand_localhost_shorthand_with_port(self):
|
||||||
|
args = parser.parse_args(args=[':3000'], env=TestEnvironment())
|
||||||
|
|
||||||
|
self.assertEqual(args.url, 'http://localhost:3000')
|
||||||
|
|
||||||
|
def test_expand_localhost_shorthand_with_path(self):
|
||||||
|
args = parser.parse_args(args=[':/path'], env=TestEnvironment())
|
||||||
|
|
||||||
|
self.assertEqual(args.url, 'http://localhost/path')
|
||||||
|
|
||||||
|
def test_expand_localhost_shorthand_with_port_and_slash(self):
|
||||||
|
args = parser.parse_args(args=[':3000/'], env=TestEnvironment())
|
||||||
|
|
||||||
|
self.assertEqual(args.url, 'http://localhost:3000/')
|
||||||
|
|
||||||
|
def test_expand_localhost_shorthand_with_port_and_path(self):
|
||||||
|
args = parser.parse_args(args=[':3000/path'], env=TestEnvironment())
|
||||||
|
|
||||||
|
self.assertEqual(args.url, 'http://localhost:3000/path')
|
||||||
|
|
||||||
|
def test_dont_expand_shorthand_ipv6_as_shorthand(self):
|
||||||
|
args = parser.parse_args(args=['::1'], env=TestEnvironment())
|
||||||
|
|
||||||
|
self.assertEqual(args.url, 'http://::1')
|
||||||
|
|
||||||
|
def test_dont_expand_longer_ipv6_as_shorthand(self):
|
||||||
|
args = parser.parse_args(
|
||||||
|
args=['::ffff:c000:0280'],
|
||||||
|
env=TestEnvironment()
|
||||||
|
)
|
||||||
|
self.assertEqual(args.url, 'http://::ffff:c000:0280')
|
||||||
|
|
||||||
|
def test_dont_expand_full_ipv6_as_shorthand(self):
|
||||||
|
args = parser.parse_args(
|
||||||
|
args=['0000:0000:0000:0000:0000:0000:0000:0001'],
|
||||||
|
env=TestEnvironment()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
args.url,
|
||||||
|
'http://0000:0000:0000:0000:0000:0000:0000:0001'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArgumentParserTestCase(unittest.TestCase):
|
class ArgumentParserTestCase(unittest.TestCase):
|
||||||
|
Reference in New Issue
Block a user